// See the file "COPYING" in the main distribution directory for copyright.
#include "zeek/Expr.h"

#include <exception>

#include "zeek/DebugLogger.h"
#include "zeek/Desc.h"
#include "zeek/Event.h"
#include "zeek/EventRegistry.h"
#include "zeek/EventTrace.h"
#include "zeek/Frame.h"
#include "zeek/Func.h"
#include "zeek/Hash.h"
#include "zeek/IPAddr.h"
#include "zeek/RE.h"
#include "zeek/Reporter.h"
#include "zeek/RunState.h"
#include "zeek/Scope.h"
#include "zeek/Stmt.h"
#include "zeek/Traverse.h"
#include "zeek/Trigger.h"
#include "zeek/Type.h"
#include "zeek/broker/Data.h"
#include "zeek/module_util.h"
#include "zeek/script_opt/Expr.h"
#include "zeek/script_opt/ScriptOpt.h"

#include "const.bif.netvar_h"

namespace zeek::detail {

class IsolationException : public std::exception {};

const char* expr_name(ExprTag t) {
    // Note that some of the names in the following have trailing spaces.
    // These are for unary operations that (1) are identified by names
    // rather than symbols, and (2) don't have custom ExprDescribe printers.
    // Adding the spaces here lets them leverage the UnaryExpr::ExprDescribe
    // method without it having to know about such expressions.
    static const char* expr_names[static_cast<int>(NUM_EXPRS)] = {
        "name",
        "const",
        "(*)",
        "++",
        "--",
        "!",
        "~",
        "+",
        "-",
        "+",
        "-",
        "add ",
        "delete ",
        "+=",
        "-=",
        "*",
        "/",
        "/", // mask operator
        "%",
        "&",
        "|",
        "^",
        "<<",
        ">>",
        "&&",
        "||",
        "<",
        "<=",
        "==",
        "!=",
        ">=",
        ">",
        "?:",
        "ref ",
        "=",
        "[]",
        "$",
        "?$",
        "[=]",
        "table()",
        "set()",
        "vector()",
        "$=",
        "in",
        "<<>>",
        "()",
        "function()",
        "event",
        "schedule",
        "coerce",
        "record_coerce ",
        "table_coerce ",
        "vector_coerce ",
        "to_any_coerce ",
        "from_any_coerce ",
        "sizeof ",
        "cast",
        "?as",
        "is",
        "[:]=",
        "inline()",
        "vec+=",
        "[]=",
        "$=",
        "$=$",
        "$+=$",
        "[=+$]",
        "from_any_vec_coerce",
        "any[]",
        "ZAM-builtin()",

        "nop", // don't add after this, it's used to compute NUM_EXPRS
    };

    if ( static_cast<int>(t) >= NUM_EXPRS ) {
        static char errbuf[512];

        // This isn't quite right - we return a static buffer,
        // so multiple calls to expr_name() could lead to confusion
        // by overwriting the buffer.  But oh well.
        snprintf(errbuf, sizeof(errbuf), "%d: not an expression tag", static_cast<int>(t));
        return errbuf;
    }

    return expr_names[static_cast<int>(t)];
}

int Expr::num_exprs = 0;

Expr::Expr(ExprTag arg_tag) : tag(arg_tag), paren(false), type(nullptr) {
    SetLocationInfo(&start_location, &end_location);
    opt_info = new ExprOptInfo();
    ++num_exprs;
}

Expr::~Expr() { delete opt_info; }

const ListExpr* Expr::AsListExpr() const {
    CHECK_TAG(tag, EXPR_LIST, "Expr::AsListExpr", expr_name)
    return static_cast<const ListExpr*>(this);
}

ListExpr* Expr::AsListExpr() {
    CHECK_TAG(tag, EXPR_LIST, "Expr::AsListExpr", expr_name)
    return static_cast<ListExpr*>(this);
}

ListExprPtr Expr::AsListExprPtr() {
    CHECK_TAG(tag, EXPR_LIST, "Expr::AsListExpr", expr_name)
    return {NewRef{}, static_cast<ListExpr*>(this)};
}

const NameExpr* Expr::AsNameExpr() const {
    CHECK_TAG(tag, EXPR_NAME, "Expr::AsNameExpr", expr_name)
    return static_cast<const NameExpr*>(this);
}

NameExpr* Expr::AsNameExpr() {
    CHECK_TAG(tag, EXPR_NAME, "Expr::AsNameExpr", expr_name)
    return static_cast<NameExpr*>(this);
}

NameExprPtr Expr::AsNameExprPtr() {
    CHECK_TAG(tag, EXPR_NAME, "Expr::AsNameExpr", expr_name)
    return {NewRef{}, static_cast<NameExpr*>(this)};
}

const ConstExpr* Expr::AsConstExpr() const {
    CHECK_TAG(tag, EXPR_CONST, "Expr::AsConstExpr", expr_name)
    return static_cast<const ConstExpr*>(this);
}

ConstExprPtr Expr::AsConstExprPtr() {
    CHECK_TAG(tag, EXPR_CONST, "Expr::AsConstExpr", expr_name)
    return {NewRef{}, static_cast<ConstExpr*>(this)};
}

const CallExpr* Expr::AsCallExpr() const {
    CHECK_TAG(tag, EXPR_CALL, "Expr::AsCallExpr", expr_name)
    return static_cast<const CallExpr*>(this);
}

const AssignExpr* Expr::AsAssignExpr() const {
    CHECK_TAG(tag, EXPR_ASSIGN, "Expr::AsAssignExpr", expr_name)
    return static_cast<const AssignExpr*>(this);
}

AssignExpr* Expr::AsAssignExpr() {
    CHECK_TAG(tag, EXPR_ASSIGN, "Expr::AsAssignExpr", expr_name)
    return static_cast<AssignExpr*>(this);
}

const IndexExpr* Expr::AsIndexExpr() const {
    CHECK_TAG(tag, EXPR_INDEX, "Expr::AsIndexExpr", expr_name)
    return static_cast<const IndexExpr*>(this);
}

IndexExpr* Expr::AsIndexExpr() {
    CHECK_TAG(tag, EXPR_INDEX, "Expr::AsIndexExpr", expr_name)
    return static_cast<IndexExpr*>(this);
}

const EventExpr* Expr::AsEventExpr() const {
    CHECK_TAG(tag, EXPR_EVENT, "Expr::AsEventExpr", expr_name)
    return static_cast<const EventExpr*>(this);
}

EventExprPtr Expr::AsEventExprPtr() {
    CHECK_TAG(tag, EXPR_EVENT, "Expr::AsEventExpr", expr_name)
    return {NewRef{}, static_cast<EventExpr*>(this)};
}

const RefExpr* Expr::AsRefExpr() const {
    CHECK_TAG(tag, EXPR_REF, "Expr::AsRefExpr", expr_name)
    return static_cast<const RefExpr*>(this);
}

RefExprPtr Expr::AsRefExprPtr() {
    CHECK_TAG(tag, EXPR_REF, "Expr::AsRefExpr", expr_name)
    return {NewRef{}, static_cast<RefExpr*>(this)};
}

bool Expr::CanAdd() const { return false; }

bool Expr::CanDel() const { return false; }

TypePtr Expr::AddType() const { return base_type(TYPE_VOID); }

TypePtr Expr::DelType() const { return base_type(TYPE_VOID); }

ValPtr Expr::Add(Frame* /* f */) { Internal("Expr::Add called"); }

ValPtr Expr::Delete(Frame* /* f */) { Internal("Expr::Delete called"); }

ExprPtr Expr::MakeLvalue() {
    if ( ! IsError() )
        ExprError("can't be assigned to");

    return ThisPtr();
}

bool Expr::InvertSense() { return false; }

void Expr::Assign(Frame* /* f */, ValPtr /* v */) { Internal("Expr::Assign called"); }

void Expr::AssignToIndex(ValPtr v1, ValPtr v2, ValPtr v3) const {
    bool iterators_invalidated;

    auto error_msg = assign_to_index(std::move(v1), std::move(v2), std::move(v3), iterators_invalidated);

    if ( iterators_invalidated )
        reporter->ExprRuntimeWarning(this, "possible loop/iterator invalidation");

    if ( error_msg )
        RuntimeErrorWithCallStack(error_msg);
}

static int get_slice_index(int idx, int len) {
    if ( abs(idx) > len )
        idx = idx > 0 ? len : 0; // Clamp maximum positive/negative indices.
    else if ( idx < 0 )
        idx += len; // Map to a positive index.

    return idx;
}

const char* assign_to_index(ValPtr v1, ValPtr v2, ValPtr v3, bool& iterators_invalidated) {
    iterators_invalidated = false;

    if ( ! v1 || ! v2 || ! v3 )
        return nullptr;

    // Hold an extra reference in case the ownership transfer
    // to the table/vector goes wrong and we still want to obtain
    // diagnostic info from the original value after the assignment
    // already unref'd.
    auto v_extra = v3;

    switch ( v1->GetType()->Tag() ) {
        case TYPE_VECTOR: {
            const ListVal* lv = v2->AsListVal();
            VectorVal* v1_vect = v1->AsVectorVal();

            if ( lv->Length() > 1 ) {
                auto len = v1_vect->Size();
                zeek_int_t first = get_slice_index(lv->Idx(0)->CoerceToInt(), len);
                zeek_int_t last = get_slice_index(lv->Idx(1)->CoerceToInt(), len);

                // Remove the elements from the vector within the slice.
                for ( auto idx = first; idx < last; idx++ )
                    v1_vect->Remove(first);

                // Insert the new elements starting at the first
                // position.

                VectorVal* v_vect = v3->AsVectorVal();

                for ( auto idx = 0u; idx < v_vect->Size(); idx++, first++ )
                    v1_vect->Insert(first, v_vect->ValAt(idx));
            }

            else if ( ! v1_vect->Assign(lv->Idx(0)->CoerceToUnsigned(), std::move(v3)) ) {
                v3 = std::move(v_extra);

                if ( v3 ) {
                    ODesc d;
                    v3->Describe(&d);
                    const auto& vt = v3->GetType();
                    auto vtt = vt->Tag();
                    std::string tn = vtt == TYPE_RECORD ? vt->GetName() : type_name(vtt);
                    return util::fmt("vector index assignment failed for invalid type '%s', value: %s", tn.data(),
                                     d.Description());
                }
                else
                    return "assignment failed with null value";
            }
            break;
        }

        case TYPE_TABLE: {
            if ( ! v1->AsTableVal()->Assign(std::move(v2), std::move(v3), true, &iterators_invalidated) ) {
                v3 = std::move(v_extra);

                if ( v3 ) {
                    ODesc d;
                    v3->Describe(&d);
                    const auto& vt = v3->GetType();
                    auto vtt = vt->Tag();
                    std::string tn = vtt == TYPE_RECORD ? vt->GetName() : type_name(vtt);
                    return util::fmt("table index assignment failed for invalid type '%s', value: %s", tn.data(),
                                     d.Description());
                }
                else
                    return "assignment failed with null value";
            }

            break;
        }

        case TYPE_STRING: return "assignment via string index accessor not allowed"; break;

        default: return "bad index expression type in assignment"; break;
    }

    return nullptr;
}

TypePtr Expr::InitType() const { return type; }

bool Expr::IsRecordElement(TypeDecl* /* td */) const { return false; }

bool Expr::IsError() const { return type && type->Tag() == TYPE_ERROR; }

void Expr::SetError() { SetType(error_type()); }

void Expr::SetError(const char* msg) {
    Error(msg);
    SetError();
}

bool Expr::IsZero() const { return IsConst() && ExprVal()->IsZero(); }

bool Expr::IsOne() const { return IsConst() && ExprVal()->IsOne(); }

void Expr::Describe(ODesc* d) const {
    if ( IsParen() && ! d->IsBinary() )
        d->Add("(");

    if ( d->IsBinary() )
        AddTag(d);

    ExprDescribe(d);

    if ( IsParen() && ! d->IsBinary() )
        d->Add(")");
}

void Expr::AddTag(ODesc* d) const {
    if ( d->IsBinary() )
        d->Add(static_cast<int>(Tag()));
    else
        d->AddSP(expr_name(Tag()));
}

void Expr::Canonicalize() {}

void Expr::SetType(TypePtr t) {
    if ( ! type || type->Tag() != TYPE_ERROR )
        type = std::move(t);
}

void Expr::ExprError(const char msg[]) {
    Error(msg);
    SetError();
}

void Expr::RuntimeError(const std::string& msg) const { reporter->ExprRuntimeError(this, "%s", msg.data()); }

void Expr::RuntimeErrorWithCallStack(const std::string& msg) const {
    auto rcs = render_call_stack();

    if ( rcs.empty() )
        reporter->ExprRuntimeError(this, "%s", msg.data());
    else {
        ODesc d;
        d.SetShort();
        Describe(&d);
        reporter->RuntimeError(GetLocationInfo(), "%s, expression: %s, call stack: %s", msg.data(), d.Description(),
                               rcs.data());
    }
}

NameExpr::NameExpr(IDPtr arg_id, bool const_init) : Expr(EXPR_NAME), id(std::move(arg_id)) {
    in_const_init = const_init;

    if ( id->IsType() )
        SetType(make_intrusive<TypeType>(id->GetType()));
    else
        SetType(id->GetType());
}

bool NameExpr::CanDel() const {
    if ( IsError() )
        return true; // avoid cascading the error report

    return GetType()->Tag() == TYPE_TABLE || GetType()->Tag() == TYPE_VECTOR;
}

ValPtr NameExpr::Delete(Frame* f) {
    auto v = Eval(f);
    if ( v ) {
        if ( GetType()->Tag() == TYPE_TABLE )
            v->AsTableVal()->RemoveAll();
        else if ( GetType()->Tag() == TYPE_VECTOR )
            v->AsVectorVal()->Resize(0);
        else
            RuntimeError("delete unsupported");
    }
    return v;
}

// This isn't in-lined to avoid needing to pull in ID.h.
const IDPtr& NameExpr::IdPtr() const { return id; }

ValPtr NameExpr::Eval(Frame* f) const {
    ValPtr v;

    if ( id->IsType() )
        return make_intrusive<TypeVal>(id->GetType(), true);

    if ( id->IsGlobal() )
        v = id->GetVal();

    else if ( f )
        v = f->GetElementByID(id);

    else
        // No frame - evaluating for purposes of resolving a compile-time
        // constant. Prevent upstream errors due to dereferencing a null
        // pointer.
        throw IsolationException();

    if ( v )
        return v;
    else {
        RuntimeError("value used but not set");
        return nullptr;
    }
}

ExprPtr NameExpr::MakeLvalue() {
    if ( id->IsType() )
        ExprError("Type name is not an lvalue");

    if ( id->IsConst() && ! in_const_init )
        ExprError("const is not a modifiable lvalue");

    if ( id->IsOption() && ! in_const_init )
        ExprError("option is not a modifiable lvalue");

    return with_location_of(make_intrusive<RefExpr>(ThisPtr()), this);
}

void NameExpr::Assign(Frame* f, ValPtr v) {
    if ( id->IsGlobal() )
        id->SetVal(std::move(v));
    else
        f->SetElement(id, std::move(v));
}

TraversalCode NameExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = id->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

void NameExpr::ExprDescribe(ODesc* d) const {
    if ( d->IsReadable() )
        d->Add(id->Name());
    else
        d->AddCS(id->Name());
}

ConstExpr::ConstExpr(ValPtr arg_val) : Expr(EXPR_CONST), val(std::move(arg_val)) {
    if ( val ) {
        if ( val->GetType()->Tag() == TYPE_LIST && val->AsListVal()->Length() == 1 )
            val = val->AsListVal()->Idx(0);

        SetType(val->GetType());
    }
    else
        SetError();
}

void ConstExpr::ExprDescribe(ODesc* d) const { val->Describe(d); }

ValPtr ConstExpr::Eval(Frame* /* f */) const { return {NewRef{}, Value()}; }

TraversalCode ConstExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

UnaryExpr::UnaryExpr(ExprTag arg_tag, ExprPtr arg_op) : Expr(arg_tag), op(std::move(arg_op)) {
    if ( op->IsError() )
        SetError();
}

ValPtr UnaryExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto v = op->Eval(f);

    if ( ! v )
        return nullptr;

    if ( is_vector(v) && Tag() != EXPR_IS && Tag() != EXPR_CAST && tag != EXPR_CAN_CONVERT &&
         // The following allows passing vectors-by-reference to
         // functions that use vector-of-any for generic vector
         // manipulation ...
         Tag() != EXPR_TO_ANY_COERCE &&
         // ... and the following to avoid vectorizing operations
         // on vector-of-any's
         Tag() != EXPR_FROM_ANY_COERCE ) {
        VectorVal* v_op = v->AsVectorVal();
        VectorTypePtr out_t;

        if ( GetType()->Tag() == TYPE_ANY )
            out_t = v->GetType<VectorType>();
        else
            out_t = GetType<VectorType>();

        auto result = make_intrusive<VectorVal>(std::move(out_t));

        for ( unsigned int i = 0; i < v_op->Size(); ++i ) {
            auto vop = v_op->ValAt(i);
            if ( vop )
                result->Assign(i, Fold(vop.get()));
            else
                result->Assign(i, nullptr);
        }

        return result;
    }
    else
        return Fold(v.get());
}

bool UnaryExpr::IsPure() const { return op->IsPure(); }

TraversalCode UnaryExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

ValPtr UnaryExpr::Fold(Val* v) const { return {NewRef{}, v}; }

void UnaryExpr::ExprDescribe(ODesc* d) const {
    bool is_coerce = Tag() == EXPR_ARITH_COERCE || Tag() == EXPR_RECORD_COERCE || Tag() == EXPR_TABLE_COERCE;

    if ( d->IsReadable() ) {
        if ( is_coerce )
            d->Add("(coerce ");
        else if ( Tag() != EXPR_REF )
            d->Add(expr_name(Tag()));
    }

    op->Describe(d);

    if ( d->IsReadable() && is_coerce ) {
        d->Add(" to ");
        GetType()->Describe(d);
        d->Add(")");
    }
}

ValPtr BinaryExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    bool is_vec1 = is_vector(v1);
    bool is_vec2 = is_vector(v2);

    if ( is_vec1 && is_vec2 ) { // fold pairs of elements
        VectorVal* v_op1 = v1->AsVectorVal();
        VectorVal* v_op2 = v2->AsVectorVal();

        if ( v_op1->Size() != v_op2->Size() ) {
            RuntimeError("vector operands are of different sizes");
            return nullptr;
        }

        auto v_result = make_intrusive<VectorVal>(GetType<VectorType>());

        for ( unsigned int i = 0; i < v_op1->Size(); ++i ) {
            auto v1_i = v_op1->ValAt(i);
            auto v2_i = v_op2->ValAt(i);
            if ( v1_i && v2_i )
                v_result->Assign(i, Fold(v_op1->ValAt(i).get(), v_op2->ValAt(i).get()));
            else
                v_result->Assign(i, nullptr);
        }

        return v_result;
    }

    if ( IsVector(GetType()->Tag()) && (is_vec1 || is_vec2) ) { // fold vector against scalar
        VectorVal* vv = (is_vec1 ? v1 : v2)->AsVectorVal();
        auto v_result = make_intrusive<VectorVal>(GetType<VectorType>());

        for ( unsigned int i = 0; i < vv->Size(); ++i ) {
            auto vv_i = vv->ValAt(i);
            if ( vv_i )
                v_result->Assign(i, is_vec1 ? Fold(vv_i.get(), v2.get()) : Fold(v1.get(), vv_i.get()));
            else
                v_result->Assign(i, nullptr);
        }

        return v_result;
    }

    // scalar op scalar
    return Fold(v1.get(), v2.get());
}

bool BinaryExpr::IsPure() const { return op1->IsPure() && op2->IsPure(); }

TraversalCode BinaryExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op1->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op2->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

void BinaryExpr::ExprDescribe(ODesc* d) const {
    op1->Describe(d);

    d->SP();
    if ( d->IsReadable() )
        d->AddSP(expr_name(Tag()));

    op2->Describe(d);
}

ValPtr BinaryExpr::Fold(Val* v1, Val* v2) const {
    auto& t1 = v1->GetType();
    InternalTypeTag it = t1->InternalType();

    if ( it == TYPE_INTERNAL_STRING )
        return StringFold(v1, v2);

    if ( t1->Tag() == TYPE_PATTERN )
        return PatternFold(v1, v2);

    if ( t1->IsSet() )
        return SetFold(v1, v2);

    if ( t1->IsTable() )
        return TableFold(v1, v2);

    if ( t1->Tag() == TYPE_VECTOR ) {
        // We only get here when using a matching vector on the RHS.
        if ( ! v2->AsVectorVal()->AddTo(v1, false) )
            Error("incompatible vector element assignment", v2);
        return {NewRef{}, v1};
    }

    if ( it == TYPE_INTERNAL_ADDR )
        return AddrFold(v1, v2);

    if ( it == TYPE_INTERNAL_SUBNET )
        return SubNetFold(v1, v2);

    zeek_int_t i1 = 0;
    zeek_int_t i2 = 0;
    zeek_int_t i3 = 0;
    zeek_uint_t u1 = 0;
    zeek_uint_t u2 = 0;
    zeek_uint_t u3 = 0;
    double d1 = 0.0;
    double d2 = 0.0;
    double d3 = 0.0;
    bool is_integral = false;
    bool is_unsigned = false;

    if ( it == TYPE_INTERNAL_INT ) {
        i1 = v1->InternalInt();
        i2 = v2->InternalInt();
        is_integral = true;
    }
    else if ( it == TYPE_INTERNAL_UNSIGNED ) {
        u1 = v1->InternalUnsigned();
        u2 = v2->InternalUnsigned();
        is_unsigned = true;
    }
    else if ( it == TYPE_INTERNAL_DOUBLE ) {
        d1 = v1->InternalDouble();
        d2 = v2->InternalDouble();
    }
    else
        RuntimeErrorWithCallStack("bad type in BinaryExpr::Fold");

    switch ( tag ) {
        // Once we have C++20, these macros can become templated lambdas.
        // NOLINTBEGIN(cppcoreguidelines-macro-usage)
#define DO_INT_FOLD(op)                                                                                                \
    if ( is_integral )                                                                                                 \
        i3 = i1 op i2;                                                                                                 \
    else if ( is_unsigned )                                                                                            \
        u3 = u1 op u2;                                                                                                 \
    else                                                                                                               \
        RuntimeErrorWithCallStack("bad type in BinaryExpr::Fold");

#define DO_UINT_FOLD(op)                                                                                               \
    if ( is_unsigned )                                                                                                 \
        u3 = u1 op u2;                                                                                                 \
    else                                                                                                               \
        RuntimeErrorWithCallStack("bad type in BinaryExpr::Fold");

#define DO_FOLD(op)                                                                                                    \
    if ( is_integral )                                                                                                 \
        i3 = i1 op i2;                                                                                                 \
    else if ( is_unsigned )                                                                                            \
        u3 = u1 op u2;                                                                                                 \
    else                                                                                                               \
        d3 = d1 op d2;

#define DO_INT_VAL_FOLD(op)                                                                                            \
    if ( is_integral )                                                                                                 \
        i3 = i1 op i2;                                                                                                 \
    else if ( is_unsigned )                                                                                            \
        i3 = u1 op u2;                                                                                                 \
    else                                                                                                               \
        i3 = d1 op d2;
            // NOLINTEND(cppcoreguidelines-macro-usage)

        case EXPR_ADD:
        case EXPR_ADD_TO: DO_FOLD(+); break;
        case EXPR_SUB:
        case EXPR_REMOVE_FROM:
            DO_FOLD(-);
            // When subtracting and the result is larger than the left
            // operand we mostly likely underflowed and log a warning.
            if ( is_unsigned && u3 > u1 )
                reporter->ExprRuntimeWarning(this, "count underflow");
            break;
        case EXPR_TIMES: DO_FOLD(*); break;
        case EXPR_DIVIDE: {
            if ( is_integral ) {
                if ( i2 == 0 )
                    RuntimeError("division by zero");

                i3 = i1 / i2;
            }

            else if ( is_unsigned ) {
                if ( u2 == 0 )
                    RuntimeError("division by zero");

                u3 = u1 / u2;
            }
            else {
                if ( d2 == 0 )
                    RuntimeError("division by zero");

                d3 = d1 / d2;
            }
        } break;

        case EXPR_MOD: {
            if ( is_integral ) {
                if ( i2 == 0 )
                    RuntimeError("modulo by zero");

                i3 = i1 % i2;
            }

            else if ( is_unsigned ) {
                if ( u2 == 0 )
                    RuntimeError("modulo by zero");

                u3 = u1 % u2;
            }

            else
                RuntimeErrorWithCallStack("bad type in BinaryExpr::Fold");
        }

        break;

        case EXPR_AND: DO_UINT_FOLD(&); break;
        case EXPR_OR: DO_UINT_FOLD(|); break;
        case EXPR_XOR: DO_UINT_FOLD(^); break;
        case EXPR_LSHIFT: {
            if ( is_integral ) {
                if ( i1 < 0 )
                    RuntimeError("left shifting a negative number is undefined");

                i3 = i1 << static_cast<zeek_uint_t>(i2);
            }

            else if ( is_unsigned )
                u3 = u1 << u2;

            else
                RuntimeErrorWithCallStack("bad type in BinaryExpr::Fold");
            break;
        }
        case EXPR_RSHIFT: {
            if ( is_integral )
                i3 = i1 >> static_cast<zeek_uint_t>(i2);

            else if ( is_unsigned )
                u3 = u1 >> u2;

            else
                RuntimeErrorWithCallStack("bad type in BinaryExpr::Fold");
            break;
        }

        case EXPR_AND_AND: DO_INT_FOLD(&&); break;
        case EXPR_OR_OR: DO_INT_FOLD(||); break;

        case EXPR_LT: DO_INT_VAL_FOLD(<); break;
        case EXPR_LE: DO_INT_VAL_FOLD(<=); break;
        case EXPR_EQ: DO_INT_VAL_FOLD(==); break;
        case EXPR_NE: DO_INT_VAL_FOLD(!=); break;
        case EXPR_GE: DO_INT_VAL_FOLD(>=); break;
        case EXPR_GT: DO_INT_VAL_FOLD(>); break;

        default: BadTag("BinaryExpr::Fold", expr_name(tag));
    }

    const auto& ret_type = IsVector(GetType()->Tag()) ? GetType()->Yield() : GetType();

    if ( ret_type->Tag() == TYPE_INTERVAL )
        return make_intrusive<IntervalVal>(d3);
    else if ( ret_type->Tag() == TYPE_TIME )
        return make_intrusive<TimeVal>(d3);
    else if ( ret_type->Tag() == TYPE_DOUBLE )
        return make_intrusive<DoubleVal>(d3);
    else if ( ret_type->InternalType() == TYPE_INTERNAL_UNSIGNED )
        return val_mgr->Count(u3);
    else if ( ret_type->Tag() == TYPE_BOOL )
        return val_mgr->Bool(i3);
    else
        return val_mgr->Int(i3);
}

ValPtr BinaryExpr::StringFold(Val* v1, Val* v2) const {
    const String* s1 = v1->AsString();
    const String* s2 = v2->AsString();
    int result = 0;

    switch ( tag ) {
#undef DO_FOLD
// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
#define DO_FOLD(sense)                                                                                                 \
    {                                                                                                                  \
        result = Bstr_cmp(s1, s2) sense 0;                                                                             \
        break;                                                                                                         \
    }
            // NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)

        case EXPR_LT: DO_FOLD(<)
        case EXPR_LE: DO_FOLD(<=)
        case EXPR_EQ: DO_FOLD(==)
        case EXPR_NE: DO_FOLD(!=)
        case EXPR_GE: DO_FOLD(>=)
        case EXPR_GT: DO_FOLD(>)

        case EXPR_ADD:
        case EXPR_ADD_TO: {
            std::vector<const String*> strings;
            strings.push_back(s1);
            strings.push_back(s2);

            return make_intrusive<StringVal>(concatenate(strings));
        }

        default: BadTag("BinaryExpr::StringFold", expr_name(tag));
    }

    return val_mgr->Bool(result);
}

ValPtr BinaryExpr::PatternFold(Val* v1, Val* v2) const {
    const RE_Matcher* re1 = v1->AsPattern();
    const RE_Matcher* re2 = v2->AsPattern();

    ValPtr res;
    if ( tag == EXPR_AND || tag == EXPR_OR ) {
        RE_Matcher* matcher = tag == EXPR_AND ? RE_Matcher_conjunction(re1, re2) : RE_Matcher_disjunction(re1, re2);
        res = make_intrusive<PatternVal>(matcher);
    }
    else if ( tag == EXPR_EQ || tag == EXPR_NE ) {
        bool cmp = strcmp(re1->PatternText(), re2->PatternText());
        res = val_mgr->Bool(tag == EXPR_EQ ? cmp == 0 : cmp != 0);
    }
    else {
        BadTag("BinaryExpr::PatternFold");
    }

    return res;
}

ValPtr BinaryExpr::SetFold(Val* v1, Val* v2) const {
    TableVal* tv1 = v1->AsTableVal();
    TableVal* tv2 = v2->AsTableVal();
    bool res = false;

    switch ( tag ) {
        case EXPR_AND: return tv1->Intersection(*tv2);

        case EXPR_OR: {
            auto rval = v1->Clone();

            if ( ! tv2->AddTo(rval.get(), false, false) )
                reporter->InternalError("set union failed to type check");

            return rval;
        }

        case EXPR_SUB: {
            auto rval = v1->Clone();

            if ( ! tv2->RemoveFrom(rval.get()) )
                reporter->InternalError("set difference failed to type check");

            return rval;
        }

        case EXPR_EQ: res = tv1->EqualTo(*tv2); break;

        case EXPR_NE: res = ! tv1->EqualTo(*tv2); break;

        case EXPR_LT: res = tv1->IsSubsetOf(*tv2) && tv1->Size() < tv2->Size(); break;

        case EXPR_LE: res = tv1->IsSubsetOf(*tv2); break;

        case EXPR_GE:
        case EXPR_GT:
            // These shouldn't happen due to canonicalization.
            reporter->InternalError("confusion over canonicalization in set comparison");
            break;

        case EXPR_ADD_TO:
            // Avoid doing the AddTo operation if tv2 is empty,
            // because then it might not type-check for trivial
            // reasons.
            if ( tv2->Size() > 0 )
                tv2->AddTo(tv1, false);
            return {NewRef{}, tv1};

        case EXPR_REMOVE_FROM:
            if ( tv2->Size() > 0 )
                tv2->RemoveFrom(tv1);
            return {NewRef{}, tv1};

        default: BadTag("BinaryExpr::SetFold", expr_name(tag)); return nullptr;
    }

    return val_mgr->Bool(res);
}

ValPtr BinaryExpr::TableFold(Val* v1, Val* v2) const {
    TableVal* tv1 = v1->AsTableVal();
    TableVal* tv2 = v2->AsTableVal();

    switch ( tag ) {
        case EXPR_ADD_TO:
            // Avoid doing the AddTo operation if tv2 is empty,
            // because then it might not type-check for trivial
            // reasons.
            if ( tv2->Size() > 0 )
                tv2->AddTo(tv1, false);
            return {NewRef{}, tv1};

        case EXPR_REMOVE_FROM:
            if ( tv2->Size() > 0 )
                tv2->RemoveFrom(tv1);
            return {NewRef{}, tv1};

        default: BadTag("BinaryExpr::TableFold", expr_name(tag));
    }

    return nullptr;
}

ValPtr BinaryExpr::AddrFold(Val* v1, Val* v2) const {
    const IPAddr& a1 = v1->AsAddr();
    const IPAddr& a2 = v2->AsAddr();
    bool result = false;

    switch ( tag ) {
        case EXPR_LT: result = a1 < a2; break;
        case EXPR_LE: result = a1 < a2 || a1 == a2; break;
        case EXPR_EQ: result = a1 == a2; break;
        case EXPR_NE: result = a1 != a2; break;
        case EXPR_GE: result = ! (a1 < a2); break;
        case EXPR_GT: result = (! (a1 < a2)) && (a1 != a2); break;

        default: BadTag("BinaryExpr::AddrFold", expr_name(tag));
    }

    return val_mgr->Bool(result);
}

ValPtr BinaryExpr::SubNetFold(Val* v1, Val* v2) const {
    const IPPrefix& n1 = v1->AsSubNet();
    const IPPrefix& n2 = v2->AsSubNet();

    bool result = n1 == n2;

    if ( tag == EXPR_NE )
        result = ! result;

    return val_mgr->Bool(result);
}

void BinaryExpr::SwapOps() {
    // We could check here whether the operator is commutative.
    using std::swap;
    swap(op1, op2);
}

void BinaryExpr::PromoteOps(TypeTag t) {
    TypeTag bt1 = op1->GetType()->Tag();
    TypeTag bt2 = op2->GetType()->Tag();

    bool is_vec1 = IsVector(bt1);
    bool is_vec2 = IsVector(bt2);

    if ( is_vec1 )
        bt1 = op1->GetType()->AsVectorType()->Yield()->Tag();
    if ( is_vec2 )
        bt2 = op2->GetType()->AsVectorType()->Yield()->Tag();

    if ( bt1 != t )
        op1 = with_location_of(make_intrusive<ArithCoerceExpr>(op1, t), op1);
    if ( bt2 != t )
        op2 = with_location_of(make_intrusive<ArithCoerceExpr>(op2, t), op2);
}

void BinaryExpr::PromoteType(TypeTag t, bool is_vector) {
    PromoteOps(t);

    if ( is_vector )
        SetType(make_intrusive<VectorType>(base_type(t)));
    else
        SetType(base_type(t));
}

void BinaryExpr::PromoteForInterval(ExprPtr& op) {
    if ( is_vector(op1) || is_vector(op2) )
        SetType(make_intrusive<VectorType>(base_type(TYPE_INTERVAL)));
    else
        SetType(base_type(TYPE_INTERVAL));

    if ( op->GetType()->Tag() != TYPE_DOUBLE )
        op = with_location_of(make_intrusive<ArithCoerceExpr>(op, TYPE_DOUBLE), op);
}

bool BinaryExpr::CheckForRHSList() {
    if ( op2->Tag() != EXPR_LIST )
        return false;

    auto lhs_t = op1->GetType();
    auto rhs = cast_intrusive<ListExpr>(op2);
    auto& rhs_exprs = rhs->Exprs();

    if ( lhs_t->Tag() == TYPE_TABLE ) {
        if ( lhs_t->IsSet() && ! rhs_exprs.empty() && same_type(lhs_t, rhs_exprs[0]->GetType()) ) {
            // This is potentially the idiom of "set1 += { set2 }"
            // or "set1 += { set2, set3, set4 }".
            op2 = {NewRef{}, rhs_exprs[0]};

            for ( auto i = 1U; i < rhs_exprs.size(); ++i ) {
                ExprPtr re_i = {NewRef{}, rhs_exprs[i]};
                op2 = with_location_of(make_intrusive<BitExpr>(EXPR_OR, op2, re_i), op2);
            }

            SetType(op1->GetType());

            return true;
        }

        if ( lhs_t->IsTable() && rhs_exprs.size() == 1 && same_type(lhs_t, rhs_exprs[0]->GetType()) ) {
            // This is the idiom of "table1 += { table2 }" (or -=).
            // Unlike for sets we don't allow more than one table
            // in the RHS list because table "union" isn't
            // well-defined.
            op2 = {NewRef{}, rhs_exprs[0]};
            SetType(op1->GetType());

            return true;
        }

        if ( lhs_t->IsTable() )
            op2 = with_location_of(make_intrusive<TableConstructorExpr>(rhs, nullptr, lhs_t), op2);
        else
            op2 = with_location_of(make_intrusive<SetConstructorExpr>(rhs, nullptr, lhs_t), op2);
    }

    else if ( lhs_t->Tag() == TYPE_VECTOR ) {
        if ( tag == EXPR_REMOVE_FROM ) {
            ExprError("constructor list not allowed for -= operations on vectors");
            return false;
        }

        op2 = with_location_of(make_intrusive<VectorConstructorExpr>(rhs, lhs_t), op2);
    }

    else {
        ExprError("invalid constructor list on RHS of assignment");
        return false;
    }

    if ( op2->IsError() ) {
        // Message should have already been generated, but propagate.
        SetError();
        return false;
    }

    // Don't bother type-checking for the degenerate case of the RHS
    // being empty, since it won't actually matter.
    if ( ! rhs_exprs.empty() && ! same_type(op1->GetType(), op2->GetType()) ) {
        ExprError("type clash for constructor list on RHS of assignment");
        return false;
    }

    SetType(op1->GetType());

    return true;
}

CloneExpr::CloneExpr(ExprPtr arg_op) : UnaryExpr(EXPR_CLONE, std::move(arg_op)) {
    if ( IsError() )
        return;

    SetType(op->GetType());
}

ValPtr CloneExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    if ( auto v = op->Eval(f) )
        return Fold(v.get());

    return nullptr;
}

ValPtr CloneExpr::Fold(Val* v) const { return v->Clone(); }

IncrExpr::IncrExpr(ExprTag arg_tag, ExprPtr arg_op) : UnaryExpr(arg_tag, arg_op->MakeLvalue()) {
    if ( IsError() )
        return;

    const auto& t = op->GetType();
    if ( ! IsIntegral(t->Tag()) )
        ExprError("requires an integral operand");
    else
        SetType(t);
}

ValPtr IncrExpr::DoSingleEval(Frame* f, Val* v) const {
    zeek_int_t k = v->CoerceToInt();

    if ( Tag() == EXPR_INCR )
        ++k;
    else {
        --k;

        if ( k < 0 && v->GetType()->InternalType() == TYPE_INTERNAL_UNSIGNED )
            reporter->ExprRuntimeWarning(this, "count underflow");
    }

    const auto& ret_type = IsVector(GetType()->Tag()) ? GetType()->Yield() : GetType();

    if ( ret_type->Tag() == TYPE_INT )
        return val_mgr->Int(k);
    else
        return val_mgr->Count(k);
}

ValPtr IncrExpr::Eval(Frame* f) const {
    auto v = op->Eval(f);

    if ( ! v )
        return nullptr;

    auto new_v = DoSingleEval(f, v.get());
    op->Assign(f, new_v);
    return new_v;
}

ComplementExpr::ComplementExpr(ExprPtr arg_op) : UnaryExpr(EXPR_COMPLEMENT, std::move(arg_op)) {
    if ( IsError() )
        return;

    const auto& t = op->GetType();
    TypeTag bt = t->Tag();

    if ( bt != TYPE_COUNT )
        ExprError("requires \"count\" operand");
    else
        SetType(base_type(TYPE_COUNT));
}

ValPtr ComplementExpr::Fold(Val* v) const { return val_mgr->Count(~v->InternalUnsigned()); }

NotExpr::NotExpr(ExprPtr arg_op) : UnaryExpr(EXPR_NOT, std::move(arg_op)) {
    if ( IsError() )
        return;

    TypeTag bt = op->GetType()->Tag();

    if ( ! IsIntegral(bt) && bt != TYPE_BOOL )
        ExprError("requires an integral or boolean operand");
    else
        SetType(base_type(TYPE_BOOL));
}

ValPtr NotExpr::Fold(Val* v) const { return val_mgr->Bool(! v->InternalInt()); }

PosExpr::PosExpr(ExprPtr arg_op) : UnaryExpr(EXPR_POSITIVE, std::move(arg_op)) {
    if ( IsError() )
        return;

    const auto& t = IsVector(op->GetType()->Tag()) ? op->GetType()->Yield() : op->GetType();

    TypeTag bt = t->Tag();
    TypePtr base_result_type;

    if ( IsIntegral(bt) )
        // Promote count and counter to int.
        base_result_type = base_type(TYPE_INT);
    else if ( bt == TYPE_INTERVAL || bt == TYPE_DOUBLE )
        base_result_type = t;
    else
        ExprError("requires an integral or double operand");

    if ( is_vector(op) )
        SetType(make_intrusive<VectorType>(std::move(base_result_type)));
    else
        SetType(std::move(base_result_type));
}

ValPtr PosExpr::Fold(Val* v) const {
    TypeTag t = v->GetType()->Tag();

    if ( t == TYPE_DOUBLE || t == TYPE_INTERVAL || t == TYPE_INT )
        return {NewRef{}, v};
    else
        return val_mgr->Int(v->CoerceToInt());
}

NegExpr::NegExpr(ExprPtr arg_op) : UnaryExpr(EXPR_NEGATE, std::move(arg_op)) {
    if ( IsError() )
        return;

    const auto& t = IsVector(op->GetType()->Tag()) ? op->GetType()->Yield() : op->GetType();

    TypeTag bt = t->Tag();
    TypePtr base_result_type;

    if ( IsIntegral(bt) )
        // Promote count and counter to int.
        base_result_type = base_type(TYPE_INT);
    else if ( bt == TYPE_INTERVAL || bt == TYPE_DOUBLE )
        base_result_type = t;
    else
        ExprError("requires an integral or double operand");

    if ( is_vector(op) )
        SetType(make_intrusive<VectorType>(std::move(base_result_type)));
    else
        SetType(std::move(base_result_type));
}

ValPtr NegExpr::Fold(Val* v) const {
    if ( v->GetType()->Tag() == TYPE_DOUBLE )
        return make_intrusive<DoubleVal>(-v->InternalDouble());
    else if ( v->GetType()->Tag() == TYPE_INTERVAL )
        return make_intrusive<IntervalVal>(-v->InternalDouble());
    else
        return val_mgr->Int(-v->CoerceToInt());
}

SizeExpr::SizeExpr(ExprPtr arg_op) : UnaryExpr(EXPR_SIZE, std::move(arg_op)) {
    if ( IsError() )
        return;

    auto& t = op->GetType();

    if ( t->Tag() == TYPE_VOID )
        SetError("cannot take size of void");
    else if ( t->Tag() == TYPE_ANY )
        SetType(base_type(TYPE_ANY));
    else if ( t->Tag() == TYPE_FILE || t->Tag() == TYPE_SUBNET || t->InternalType() == TYPE_INTERNAL_DOUBLE )
        SetType(base_type(TYPE_DOUBLE));
    else
        SetType(base_type(TYPE_COUNT));
}

ValPtr SizeExpr::Eval(Frame* f) const {
    auto v = op->Eval(f);

    if ( ! v )
        return nullptr;

    return Fold(v.get());
}

ValPtr SizeExpr::Fold(Val* v) const { return v->SizeVal(); }

// Fill op1 and op2 type tags into bt1 and bt2.
//
// If both operands are vectors, use their yield type tag. If
// either, but not both operands, is a vector, cause an expression
// error and return false.
static bool get_types_from_scalars_or_vectors(Expr* e, TypeTag& bt1, TypeTag& bt2) {
    bt1 = e->GetOp1()->GetType()->Tag();
    bt2 = e->GetOp2()->GetType()->Tag();

    if ( IsVector(bt1) && IsVector(bt2) ) {
        bt1 = e->GetOp1()->GetType()->AsVectorType()->Yield()->Tag();
        bt2 = e->GetOp2()->GetType()->AsVectorType()->Yield()->Tag();
    }
    else if ( IsVector(bt1) || IsVector(bt2) ) {
        e->Error("cannot mix vector and scalar operands");
        e->SetError();
        return false;
    }

    return true;
}

AddExpr::AddExpr(ExprPtr arg_op1, ExprPtr arg_op2) : BinaryExpr(EXPR_ADD, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    TypePtr base_result_type;

    if ( (bt2 == TYPE_INTERVAL && (bt1 == TYPE_TIME || bt1 == TYPE_INTERVAL)) )
        base_result_type = base_type(bt1); // NOLINT(bugprone-branch-clone)
    else if ( bt2 == TYPE_TIME && bt1 == TYPE_INTERVAL )
        base_result_type = base_type(bt2);
    else if ( BothArithmetic(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));
    else if ( BothString(bt1, bt2) )
        base_result_type = base_type(bt1);
    else
        ExprError("requires arithmetic operands");

    if ( base_result_type ) {
        if ( is_vector(op1) )
            SetType(make_intrusive<VectorType>(std::move(base_result_type)));
        else
            SetType(std::move(base_result_type));
    }
}

void AddExpr::Canonicalize() {
    if ( expr_greater(op2.get(), op1.get()) ||
         (op1->GetType()->Tag() == TYPE_INTERVAL && op2->GetType()->Tag() == TYPE_TIME) ||
         (op2->IsConst() && ! is_vector(op2->ExprVal()) && ! op1->IsConst()) )
        SwapOps();
}

AggrAddExpr::AggrAddExpr(ExprPtr _op) : AggrAddDelExpr(EXPR_AGGR_ADD, std::move(_op)) {
    if ( ! op->IsError() && ! op->CanAdd() )
        ExprError("illegal add expression");

    SetType(op->AddType());
}

ValPtr AggrAddExpr::Eval(Frame* f) const { return op->Add(f); }

AggrDelExpr::AggrDelExpr(ExprPtr _op) : AggrAddDelExpr(EXPR_AGGR_DEL, std::move(_op)) {
    if ( ! op->IsError() && ! op->CanDel() )
        Error("illegal delete expression");

    SetType(op->DelType());
}

ValPtr AggrDelExpr::Eval(Frame* f) const { return op->Delete(f); }

// True if we should treat LHS += RHS as add-every-element-of-RHS-to-LHS.
// False for the alternative, add-RHS-as-one-element-to-LHS.
//
// Assumes (1) LHS has already been confirmed as a vector, (2) the
// "LHS += RHS" expression has been type-checked.

static bool is_element_wise_vector_append(const TypePtr& lhs, const TypePtr& rhs) {
    if ( ! IsVector(rhs->Tag()) )
        // Can't be add-every-element since RHS isn't even a vector.
        return false;

    if ( ! same_type(lhs, rhs) )
        // Can't be add-every-element since they're different types of vectors.
        return false;

    if ( lhs->Yield()->Tag() != TYPE_VECTOR )
        // LHS is not a vector-of-vector, and RHS is a vector, so
        // clearly we're doing element-wise-append.
        return true;

    if ( rhs->AsVectorType()->IsUnspecifiedVector() )
        // This is a vector-of-vector-of-X += vector() construct.
        // It is *not* treated as element-wise-append of an empty RHS,
        // instead append an empty vector to the LHS.
        return false;

    // RHS is a compatible element-wise-append vector for LHS.
    return true;
}

AddToExpr::AddToExpr(ExprPtr arg_op1, ExprPtr arg_op2)
    : BinaryExpr(EXPR_ADD_TO, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    auto& t1 = op1->GetType();
    auto& t2 = op2->GetType();
    TypeTag bt1 = t1->Tag();
    TypeTag bt2 = t2->Tag();

    if ( bt1 != TYPE_TABLE && bt1 != TYPE_VECTOR && bt1 != TYPE_PATTERN )
        op1 = op1->MakeLvalue();

    if ( BothArithmetic(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));
    else if ( BothString(bt1, bt2) || BothInterval(bt1, bt2) )
        SetType(base_type(bt1));

    else if ( bt2 == TYPE_LIST )
        (void)CheckForRHSList();

    else if ( bt1 == TYPE_TABLE ) {
        if ( same_type(t1, t2) )
            SetType(t1);
        else
            ExprError("RHS type mismatch for table/set +=");
    }

    else if ( bt1 == TYPE_PATTERN ) {
        if ( bt2 != TYPE_PATTERN )
            ExprError("pattern += op requires op to be a pattern");
        else
            SetType(t1);
    }

    else if ( IsVector(bt1) ) {
        // Treat += of two vectors as appending each element
        // of the RHS to the LHS if types agree.
        if ( is_element_wise_vector_append(t1, t2) ) {
            SetType(t1);
            return;
        }

        is_vector_elem_append = true;

        bt1 = t1->AsVectorType()->Yield()->Tag();

        if ( IsArithmetic(bt1) ) {
            if ( IsArithmetic(bt2) ) {
                if ( bt2 != bt1 )
                    op2 = with_location_of(make_intrusive<ArithCoerceExpr>(op2, bt1), op2);

                SetType(t1);
            }

            else
                ExprError("appending non-arithmetic to arithmetic vector");
        }

        else if ( bt1 != bt2 && bt1 != TYPE_ANY )
            ExprError(util::fmt("incompatible vector append: %s and %s", type_name(bt1), type_name(bt2)));

        else
            SetType(t1);
    }

    else
        ExprError("requires two arithmetic or two string operands");
}

ValPtr AddToExpr::Eval(Frame* f) const {
    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    if ( is_vector_elem_append ) {
        VectorVal* vv = v1->AsVectorVal();

        if ( ! vv->Assign(vv->Size(), v2) )
            RuntimeError("type-checking failed in vector append");

        return v1;
    }

    if ( type->Tag() == TYPE_PATTERN ) {
        v2->AddTo(v1.get(), false);
        return v1;
    }

    if ( auto result = Fold(v1.get(), v2.get()) ) {
        op1->Assign(f, result);
        return result;
    }
    else
        return nullptr;
}

SubExpr::SubExpr(ExprPtr arg_op1, ExprPtr arg_op2) : BinaryExpr(EXPR_SUB, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    const auto& t1 = op1->GetType();
    const auto& t2 = op2->GetType();

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    TypePtr base_result_type;

    if ( bt2 == TYPE_INTERVAL && (bt1 == TYPE_TIME || bt1 == TYPE_INTERVAL) )
        base_result_type = base_type(bt1);

    else if ( bt1 == TYPE_TIME && bt2 == TYPE_TIME )
        SetType(base_type(TYPE_INTERVAL));

    else if ( t1->IsSet() && t2->IsSet() ) {
        if ( same_type(t1, t2) )
            SetType(op1->GetType());
        else
            ExprError("incompatible \"set\" operands");
    }

    else if ( BothArithmetic(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));

    else
        ExprError("requires arithmetic operands");

    if ( base_result_type ) {
        if ( is_vector(op1) )
            SetType(make_intrusive<VectorType>(std::move(base_result_type)));
        else
            SetType(std::move(base_result_type));
    }
}

RemoveFromExpr::RemoveFromExpr(ExprPtr arg_op1, ExprPtr arg_op2)
    : BinaryExpr(EXPR_REMOVE_FROM, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    auto& t1 = op1->GetType();
    auto& t2 = op2->GetType();
    TypeTag bt1 = t1->Tag();
    TypeTag bt2 = t2->Tag();

    if ( bt1 != TYPE_TABLE )
        op1 = op1->MakeLvalue();

    if ( BothArithmetic(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));
    else if ( BothInterval(bt1, bt2) )
        SetType(base_type(bt1));

    else if ( bt2 == TYPE_LIST )
        (void)CheckForRHSList();

    else if ( bt1 == TYPE_TABLE ) {
        if ( same_type(t1, t2) )
            SetType(t1);
        else
            ExprError("RHS type mismatch for table/set -=");
    }

    else
        ExprError("requires two arithmetic operands");
}

ValPtr RemoveFromExpr::Eval(Frame* f) const {
    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    if ( auto result = Fold(v1.get(), v2.get()) ) {
        op1->Assign(f, result);
        return result;
    }
    else
        return nullptr;
}

TimesExpr::TimesExpr(ExprPtr arg_op1, ExprPtr arg_op2)
    : BinaryExpr(EXPR_TIMES, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    Canonicalize();

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( bt1 == TYPE_INTERVAL || bt2 == TYPE_INTERVAL ) {
        if ( IsArithmetic(bt1) || IsArithmetic(bt2) )
            PromoteForInterval(IsArithmetic(bt1) ? op1 : op2);
        else
            ExprError("multiplication with interval requires arithmetic operand");
    }
    else if ( BothArithmetic(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));
    else
        ExprError("requires arithmetic operands");
}

void TimesExpr::Canonicalize() {
    if ( expr_greater(op2.get(), op1.get()) || op2->GetType()->Tag() == TYPE_INTERVAL ||
         (op2->IsConst() && ! is_vector(op2->ExprVal()) && ! op1->IsConst()) )
        SwapOps();
}

DivideExpr::DivideExpr(ExprPtr arg_op1, ExprPtr arg_op2)
    : BinaryExpr(EXPR_DIVIDE, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( bt1 == TYPE_INTERVAL || bt2 == TYPE_INTERVAL ) {
        if ( IsArithmetic(bt1) || IsArithmetic(bt2) )
            PromoteForInterval(IsArithmetic(bt1) ? op1 : op2);
        else if ( bt1 == TYPE_INTERVAL && bt2 == TYPE_INTERVAL ) {
            if ( is_vector(op1) )
                SetType(make_intrusive<VectorType>(base_type(TYPE_DOUBLE)));
            else
                SetType(base_type(TYPE_DOUBLE));
        }
        else
            ExprError("division of interval requires arithmetic operand");
    }

    else if ( BothArithmetic(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));

    else
        ExprError("requires arithmetic operands");
}

MaskExpr::MaskExpr(ExprPtr arg_op1, ExprPtr arg_op2) : BinaryExpr(EXPR_MASK, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( bt1 == TYPE_ADDR && ! is_vector(op2) && (bt2 == TYPE_COUNT || bt2 == TYPE_INT) )
        SetType(base_type(TYPE_SUBNET));
    else
        ExprError("requires address LHS and count/int RHS");
}

ValPtr MaskExpr::AddrFold(Val* v1, Val* v2) const {
    auto mask = GetMask(v2);
    auto& a = v1->AsAddr();

    if ( a.GetFamily() == IPv4 ) {
        if ( mask > 32 )
            RuntimeError(util::fmt("bad IPv4 subnet prefix length: %" PRIu32, mask));
    }
    else {
        if ( mask > 128 )
            RuntimeError(util::fmt("bad IPv6 subnet prefix length: %" PRIu32, mask));
    }

    return make_intrusive<SubNetVal>(a, mask);
}

uint32_t MaskExpr::GetMask(const Val* v) const {
    if ( v->GetType()->Tag() == TYPE_COUNT )
        return static_cast<uint32_t>(v->InternalUnsigned());
    else
        return static_cast<uint32_t>(v->InternalInt());
}

ModExpr::ModExpr(ExprPtr arg_op1, ExprPtr arg_op2) : BinaryExpr(EXPR_MOD, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( BothIntegral(bt1, bt2) )
        PromoteType(max_type(bt1, bt2), is_vector(op1) || is_vector(op2));
    else
        ExprError("requires integral operands");
}

BoolExpr::BoolExpr(ExprTag arg_tag, ExprPtr arg_op1, ExprPtr arg_op2)
    : BinaryExpr(arg_tag, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( BothBool(bt1, bt2) ) {
        if ( is_vector(op1) )
            SetType(make_intrusive<VectorType>(base_type(TYPE_BOOL)));
        else
            SetType(base_type(TYPE_BOOL));
    }
    else
        ExprError("requires boolean operands");
}

ValPtr BoolExpr::DoSingleEval(Frame* f, ValPtr v1, Expr* op2) const {
    if ( ! v1 )
        return nullptr;

    if ( tag == EXPR_AND_AND ) {
        if ( v1->IsZero() )
            return v1;
        else
            return op2->Eval(f);
    }

    else {
        if ( v1->IsZero() )
            return op2->Eval(f);
        else
            return v1;
    }
}

ValPtr BoolExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    bool is_vec1 = is_vector(op1);
    bool is_vec2 = is_vector(op2);

    // Handle scalar op scalar
    if ( ! is_vec1 && ! is_vec2 )
        return DoSingleEval(f, std::move(v1), op2.get());

    // Both are vectors.
    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    VectorVal* vec_v1 = v1->AsVectorVal();
    VectorVal* vec_v2 = v2->AsVectorVal();

    if ( vec_v1->Size() != vec_v2->Size() ) {
        RuntimeError("vector operands have different sizes");
        return nullptr;
    }

    auto result = make_intrusive<VectorVal>(GetType<VectorType>());
    result->Resize(vec_v1->Size());

    for ( unsigned int i = 0; i < vec_v1->Size(); ++i ) {
        const auto op1 = vec_v1->BoolAt(i);
        const auto op2 = vec_v2->BoolAt(i);

        bool local_result = (tag == EXPR_AND_AND) ? (op1 && op2) : (op1 || op2);

        result->Assign(i, val_mgr->Bool(local_result));
    }

    return result;
}

BitExpr::BitExpr(ExprTag arg_tag, ExprPtr arg_op1, ExprPtr arg_op2)
    : BinaryExpr(arg_tag, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    const auto& t1 = op1->GetType();
    const auto& t2 = op2->GetType();

    TypeTag bt1 = t1->Tag();

    if ( IsVector(bt1) )
        bt1 = t1->AsVectorType()->Yield()->Tag();

    TypeTag bt2 = t2->Tag();

    if ( IsVector(bt2) )
        bt2 = t2->AsVectorType()->Yield()->Tag();

    if ( tag == EXPR_LSHIFT || tag == EXPR_RSHIFT ) {
        if ( (is_vector(op1) || is_vector(op2)) && ! (is_vector(op1) && is_vector(op2)) )
            ExprError("cannot mix vectors and scalars for shift operations");

        if ( IsIntegral(bt1) && bt2 == TYPE_COUNT ) {
            if ( is_vector(op1) || is_vector(op2) )
                SetType(make_intrusive<VectorType>(base_type(bt1)));
            else {
                SetType(base_type(bt1));
                if ( bt1 != bt2 )
                    op2 = make_intrusive<ArithCoerceExpr>(op2, bt1);
            }
        }

        else if ( IsIntegral(bt1) && bt2 == TYPE_INT )
            ExprError("requires \"count\" right operand");

        else
            ExprError("requires integral operands");

        return; // because following scalar check isn't apt
    }

    if ( (bt1 == TYPE_COUNT) && (bt2 == TYPE_COUNT) ) {
        if ( is_vector(op1) || is_vector(op2) )
            SetType(make_intrusive<VectorType>(base_type(TYPE_COUNT)));
        else
            SetType(base_type(TYPE_COUNT));
    }

    else if ( bt1 == TYPE_PATTERN ) {
        if ( bt2 != TYPE_PATTERN )
            ExprError("cannot mix pattern and non-pattern operands");
        else if ( tag == EXPR_XOR )
            ExprError("'^' operator does not apply to patterns");
        else
            SetType(base_type(TYPE_PATTERN));
    }

    else if ( t1->IsSet() && t2->IsSet() ) {
        if ( same_type(t1, t2) )
            SetType(op1->GetType());
        else
            ExprError("incompatible \"set\" operands");
    }

    else
        ExprError("requires \"count\" or compatible \"set\" operands");
}

CmpExpr::CmpExpr(ExprTag tag, ExprPtr _op1, ExprPtr _op2) : BinaryExpr(tag, std::move(_op1), std::move(_op2)) {
    if ( IsError() )
        return;

    Canonicalize();

    if ( is_vector(op1) )
        SetType(make_intrusive<VectorType>(base_type(TYPE_BOOL)));
    else
        SetType(base_type(TYPE_BOOL));
}

void CmpExpr::Canonicalize() {
    if ( tag == EXPR_EQ || tag == EXPR_NE ) {
        if ( op2->GetType()->Tag() == TYPE_PATTERN )
            SwapOps(); //NOLINT(bugprone-branch-clone)

        else if ( op1->GetType()->Tag() == TYPE_PATTERN )
            ;

        else if ( expr_greater(op2.get(), op1.get()) )
            SwapOps();
    }

    else if ( tag == EXPR_GT ) {
        SwapOps();
        tag = EXPR_LT;
    }

    else if ( tag == EXPR_GE ) {
        SwapOps();
        tag = EXPR_LE;
    }
}

EqExpr::EqExpr(ExprTag arg_tag, ExprPtr arg_op1, ExprPtr arg_op2)
    : CmpExpr(arg_tag, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    const auto& t1 = op1->GetType();
    const auto& t2 = op2->GetType();

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( BothArithmetic(bt1, bt2) )
        PromoteOps(max_type(bt1, bt2));

    else if ( EitherArithmetic(bt1, bt2) &&
              // Allow comparisons with zero.
              ((bt1 == TYPE_TIME && op2->IsZero()) || (bt2 == TYPE_TIME && op1->IsZero())) )
        PromoteOps(TYPE_TIME);

    else if ( bt1 == bt2 ) {
        switch ( bt1 ) {
            case TYPE_BOOL:
            case TYPE_TIME:
            case TYPE_INTERVAL:
            case TYPE_STRING:
            case TYPE_PORT:
            case TYPE_ADDR:
            case TYPE_SUBNET:
            case TYPE_ERROR:
            case TYPE_PATTERN:
            case TYPE_FUNC: break;

            case TYPE_ENUM:
                if ( ! same_type(t1, t2) )
                    ExprError("illegal enum comparison");
                break;

            case TYPE_TABLE:
                if ( t1->IsSet() && t2->IsSet() ) {
                    if ( ! same_type(t1, t2) )
                        ExprError("incompatible sets in comparison");
                    break;
                }

                // FALL THROUGH

            default: ExprError("illegal comparison");
        }
    }

    else if ( (bt1 == TYPE_PATTERN && bt2 == TYPE_STRING) || (bt1 == TYPE_STRING && bt2 == TYPE_PATTERN) ) {
        if ( op1->GetType()->Tag() == TYPE_VECTOR )
            ExprError("cannot compare string vectors with pattern vectors");
    }

    else
        ExprError("type clash in comparison");
}

ValPtr EqExpr::Fold(Val* v1, Val* v2) const {
    if ( op1->GetType()->Tag() == TYPE_PATTERN ) {
        if ( op2->GetType()->Tag() == TYPE_PATTERN ) {
            auto re1 = v1->As<PatternVal*>();
            auto re2 = v2->As<PatternVal*>();
            return val_mgr->Bool(strcmp(re1->Get()->PatternText(), re2->Get()->PatternText()) == 0);
        }
        else {
            auto re = v1->As<PatternVal*>();
            const String* s = v2->AsString();
            if ( tag == EXPR_EQ )
                return val_mgr->Bool(re->MatchExactly(s));
            else
                return val_mgr->Bool(! re->MatchExactly(s));
        }
    }
    else if ( op1->GetType()->Tag() == TYPE_FUNC ) {
        auto res = v1->AsFunc() == v2->AsFunc();
        return val_mgr->Bool(tag == EXPR_EQ ? res : ! res);
    }

    else
        return BinaryExpr::Fold(v1, v2);
}

bool EqExpr::InvertSense() {
    tag = (tag == EXPR_EQ ? EXPR_NE : EXPR_EQ);
    return true;
}

RelExpr::RelExpr(ExprTag arg_tag, ExprPtr arg_op1, ExprPtr arg_op2)
    : CmpExpr(arg_tag, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    const auto& t1 = op1->GetType();
    const auto& t2 = op2->GetType();

    TypeTag bt1;
    TypeTag bt2;
    if ( ! get_types_from_scalars_or_vectors(this, bt1, bt2) )
        return;

    if ( BothArithmetic(bt1, bt2) )
        PromoteOps(max_type(bt1, bt2));

    else if ( t1->IsSet() && t2->IsSet() ) {
        if ( ! same_type(t1, t2) )
            ExprError("incompatible sets in comparison");
    }

    else if ( bt1 != bt2 )
        ExprError("operands must be of the same type");

    else if ( bt1 != TYPE_TIME && bt1 != TYPE_INTERVAL && bt1 != TYPE_PORT && bt1 != TYPE_ADDR && bt1 != TYPE_STRING )
        ExprError("illegal comparison");
}

bool RelExpr::InvertSense() {
    switch ( tag ) {
        case EXPR_LT: tag = EXPR_GE; return true;
        case EXPR_LE: tag = EXPR_GT; return true;
        case EXPR_GE: tag = EXPR_LT; return true;
        case EXPR_GT: tag = EXPR_LE; return true;

        default: return false;
    }
}

CondExpr::CondExpr(ExprPtr arg_op1, ExprPtr arg_op2, ExprPtr arg_op3)
    : Expr(EXPR_COND), op1(std::move(arg_op1)), op2(std::move(arg_op2)), op3(std::move(arg_op3)) {
    TypeTag bt1 = op1->GetType()->Tag();

    if ( IsVector(bt1) )
        bt1 = op1->GetType()->AsVectorType()->Yield()->Tag();

    if ( op1->IsError() || op2->IsError() || op3->IsError() )
        SetError();

    else if ( bt1 != TYPE_BOOL )
        ExprError("requires boolean conditional");

    else {
        TypeTag bt2 = op2->GetType()->Tag();
        TypeTag bt3 = op3->GetType()->Tag();

        if ( is_vector(op1) ) {
            if ( ! (is_vector(op2) && is_vector(op3)) ) {
                ExprError("vector conditional requires vector alternatives");
                return;
            }

            bt2 = op2->GetType()->AsVectorType()->Yield()->Tag();
            bt3 = op3->GetType()->AsVectorType()->Yield()->Tag();
        }

        if ( BothArithmetic(bt2, bt3) ) {
            TypeTag t = max_type(bt2, bt3);
            if ( bt2 != t )
                op2 = with_location_of(make_intrusive<ArithCoerceExpr>(op2, t), op2);
            if ( bt3 != t )
                op3 = with_location_of(make_intrusive<ArithCoerceExpr>(op3, t), op3);

            if ( is_vector(op1) )
                SetType(make_intrusive<VectorType>(base_type(t)));
            else
                SetType(base_type(t));
        }

        else if ( bt2 != bt3 )
            ExprError("operands must be of the same type");

        else {
            if ( is_vector(op1) ) {
                ExprError("vector conditional type clash between alternatives");
                return;
            }

            if ( bt2 == zeek::TYPE_TABLE ) {
                auto tt2 = op2->GetType<TableType>();
                auto tt3 = op3->GetType<TableType>();

                if ( tt2->IsUnspecifiedTable() )
                    op2 = with_location_of(make_intrusive<TableCoerceExpr>(op2, std::move(tt3)), op2);

                else if ( tt3->IsUnspecifiedTable() )
                    op3 = with_location_of(make_intrusive<TableCoerceExpr>(op3, std::move(tt2)), op3);

                else if ( ! same_type(op2->GetType(), op3->GetType()) )
                    ExprError("operands must be of the same type");
            }
            else if ( bt2 == zeek::TYPE_VECTOR ) {
                auto vt2 = op2->GetType<VectorType>();
                auto vt3 = op3->GetType<VectorType>();

                if ( vt2->IsUnspecifiedVector() )
                    op2 = with_location_of(make_intrusive<VectorCoerceExpr>(op2, std::move(vt3)), op2);
                else if ( vt3->IsUnspecifiedVector() )
                    op3 = with_location_of(make_intrusive<VectorCoerceExpr>(op3, std::move(vt2)), op3);
                else if ( ! same_type(op2->GetType(), op3->GetType()) )
                    ExprError("operands must be of the same type");
            }
            else if ( ! same_type(op2->GetType(), op3->GetType()) )
                // Records could potentially also coerce, but may have some cases
                // where the coercion direction is ambiguous.
                ExprError("operands must be of the same type");

            if ( ! IsError() )
                SetType(op2->GetType());
        }
    }
}

ValPtr CondExpr::Eval(Frame* f) const {
    if ( ! is_vector(op1) ) {
        // Scalar case
        auto false_eval = op1->Eval(f)->IsZero();
        return (false_eval ? op3 : op2)->Eval(f);
    }

    // Vector case: no mixed scalar/vector cases allowed
    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    auto v3 = op3->Eval(f);

    if ( ! v3 )
        return nullptr;

    VectorVal* cond = v1->AsVectorVal();
    VectorVal* a = v2->AsVectorVal();
    VectorVal* b = v3->AsVectorVal();

    if ( cond->Size() != a->Size() || a->Size() != b->Size() ) {
        RuntimeError("vectors in conditional expression have different sizes");
        return nullptr;
    }

    auto result = make_intrusive<VectorVal>(GetType<VectorType>());
    result->Resize(cond->Size());

    for ( unsigned int i = 0; i < cond->Size(); ++i ) {
        auto local_cond = cond->BoolAt(i);
        auto v = local_cond ? a->ValAt(i) : b->ValAt(i);
        result->Assign(i, v);
    }

    return result;
}

bool CondExpr::IsPure() const { return op1->IsPure() && op2->IsPure() && op3->IsPure(); }

TraversalCode CondExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op1->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op2->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op3->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

void CondExpr::ExprDescribe(ODesc* d) const {
    op1->Describe(d);
    d->AddSP(" ?");
    op2->Describe(d);
    d->AddSP(" :");
    op3->Describe(d);
}

RefExpr::RefExpr(ExprPtr arg_op) : UnaryExpr(EXPR_REF, std::move(arg_op)) {
    if ( IsError() )
        return;

    if ( ! is_assignable(op->GetType()->Tag()) )
        ExprError("illegal assignment target");
    else
        SetType(op->GetType());
}

ExprPtr RefExpr::MakeLvalue() { return ThisPtr(); }

void RefExpr::Assign(Frame* f, ValPtr v) { op->Assign(f, std::move(v)); }

AssignExpr::AssignExpr(ExprPtr arg_op1, ExprPtr arg_op2, bool arg_is_init, ValPtr arg_val, const AttributesPtr& attrs,
                       bool typecheck)
    : BinaryExpr(EXPR_ASSIGN, arg_is_init ? std::move(arg_op1) : arg_op1->MakeLvalue(), std::move(arg_op2)) {
    val = nullptr;
    is_init = arg_is_init;

    if ( IsError() )
        return;

    if ( arg_val )
        SetType(arg_val->GetType());
    else
        SetType(op1->GetType());

    if ( is_init ) {
        SetLocationInfo(op1->GetLocationInfo(), op2->GetLocationInfo());
        return;
    }

    if ( op2->Tag() == EXPR_LIST && CheckForRHSList() ) {
        if ( op2->Tag() == EXPR_TABLE_CONSTRUCTOR )
            cast_intrusive<TableConstructorExpr>(op2)->SetAttrs(attrs);
        else if ( op2->Tag() == EXPR_SET_CONSTRUCTOR )
            cast_intrusive<SetConstructorExpr>(op2)->SetAttrs(attrs);
    }

    else if ( typecheck )
        // We discard the status from TypeCheck since it has already
        // generated error messages.
        (void)TypeCheck(attrs);

    val = std::move(arg_val);

    SetLocationInfo(op1->GetLocationInfo(), op2->GetLocationInfo());
}

bool AssignExpr::TypeCheck(const AttributesPtr& attrs) {
    TypeTag bt1 = op1->GetType()->Tag();
    TypeTag bt2 = op2->GetType()->Tag();

    if ( bt2 == TYPE_VOID ) {
        ExprError("can't assign void value");
        return false;
    }

    if ( bt1 == TYPE_LIST && bt2 == TYPE_ANY )
        // This is ok because we cannot explicitly declare lists on
        // the script level.
        return true;

    // This should be one of them, but not both (i.e. XOR)
    if ( ((bt1 == TYPE_ENUM) ^ (bt2 == TYPE_ENUM)) ) {
        ExprError("can't convert to/from enumerated type");
        return false;
    }

    if ( IsArithmetic(bt1) )
        return TypeCheckArithmetics(bt1, bt2);

    if ( bt1 == TYPE_TIME && IsArithmetic(bt2) && op2->IsZero() ) { // Allow assignments to zero as a special case.
        op2 = with_location_of(make_intrusive<ArithCoerceExpr>(op2, bt1), op2);
        return true;
    }

    if ( bt1 == TYPE_TABLE && bt2 == bt1 && op2->GetType()->AsTableType()->IsUnspecifiedTable() ) {
        op2 = with_location_of(make_intrusive<TableCoerceExpr>(op2, op1->GetType<TableType>()), op2);
        return true;
    }

    if ( bt1 == TYPE_VECTOR ) {
        if ( bt2 == bt1 && op2->GetType()->AsVectorType()->IsUnspecifiedVector() ) {
            op2 = with_location_of(make_intrusive<VectorCoerceExpr>(op2, op1->GetType<VectorType>()), op2);
            return true;
        }

        if ( op2->Tag() == EXPR_LIST ) {
            op2 = with_location_of(make_intrusive<VectorConstructorExpr>(cast_intrusive<ListExpr>(op2), op1->GetType()),
                                   op2);
            return true;
        }
    }

    if ( bt1 == TYPE_RECORD && bt2 == TYPE_RECORD ) {
        if ( same_type(op1->GetType(), op2->GetType()) )
            return true;

        // Need to coerce.
        op2 = with_location_of(make_intrusive<RecordCoerceExpr>(op2, op1->GetType<RecordType>()), op2);
        return true;
    }

    if ( ! same_type(op1->GetType(), op2->GetType()) ) {
        if ( bt1 == TYPE_TABLE && bt2 == TYPE_TABLE ) {
            if ( op2->Tag() == EXPR_SET_CONSTRUCTOR ) {
                // Some elements in constructor list must not match, see if
                // we can create a new constructor now that the expected type
                // of LHS is known and let it do coercions where possible.
                auto sce = cast_intrusive<SetConstructorExpr>(op2);
                auto ctor_list = cast_intrusive<ListExpr>(sce->GetOp1());

                if ( ! ctor_list )
                    Internal("failed typecast to ListExpr");

                std::unique_ptr<std::vector<AttrPtr>> attr_copy;

                if ( sce->GetAttrs() ) {
                    const auto& a = sce->GetAttrs()->GetAttrs();
                    attr_copy = std::make_unique<std::vector<AttrPtr>>(a);
                }

                int errors_before = reporter->Errors();
                op2 = with_location_of(make_intrusive<SetConstructorExpr>(ctor_list, std::move(attr_copy),
                                                                          op1->GetType()),
                                       op2);
                int errors_after = reporter->Errors();

                if ( errors_after > errors_before ) {
                    ExprError("type clash in assignment");
                    return false;
                }

                return true;
            }
        }

        ExprError("type clash in assignment");
        return false;
    }

    return true;
}

bool AssignExpr::TypeCheckArithmetics(TypeTag bt1, TypeTag bt2) {
    if ( ! IsArithmetic(bt2) ) {
        ExprError(
            util::fmt("assignment of non-arithmetic value to arithmetic (%s/%s)", type_name(bt1), type_name(bt2)));
        return false;
    }

    if ( bt1 == TYPE_DOUBLE ) {
        PromoteOps(TYPE_DOUBLE);
        return true;
    }

    if ( bt2 == TYPE_DOUBLE ) {
        Warn("dangerous assignment of double to integral");
        op2 = with_location_of(make_intrusive<ArithCoerceExpr>(op2, bt1), op2);
        bt2 = op2->GetType()->Tag();
    }

    if ( bt1 == TYPE_INT )
        PromoteOps(TYPE_INT);
    else {
        if ( bt2 == TYPE_INT ) {
            Warn("dangerous assignment of integer to count");
            op2 = with_location_of(make_intrusive<ArithCoerceExpr>(op2, bt1), op2);
        }

        // Assignment of count to counter or vice
        // versa is allowed, and requires no
        // coercion.
    }

    return true;
}

ValPtr AssignExpr::Eval(Frame* f) const {
    if ( is_init ) {
        RuntimeError("illegal assignment in initialization");
        return nullptr;
    }

    if ( auto v = op2->Eval(f) ) {
        op1->Assign(f, v);

        if ( val )
            return val;

        return v;
    }
    else
        return nullptr;
}

TypePtr AssignExpr::InitType() const {
    if ( op1->Tag() != EXPR_LIST ) {
        Error("bad initializer, first operand should be a list");
        return nullptr;
    }

    const auto& tl = op1->GetType();
    if ( tl->Tag() != TYPE_LIST )
        Internal("inconsistent list expr in AssignExpr::InitType");

    return make_intrusive<TableType>(IntrusivePtr{NewRef{}, tl->AsTypeList()}, op2->GetType());
}

bool AssignExpr::IsRecordElement(TypeDecl* td) const {
    if ( op1->Tag() == EXPR_NAME ) {
        if ( td ) {
            const NameExpr* n = static_cast<const NameExpr*>(op1.get());
            td->type = op2->GetType();
            td->id = util::copy_string(n->Id()->Name());
        }

        return true;
    }

    return false;
}

IndexSliceAssignExpr::IndexSliceAssignExpr(ExprPtr op1, ExprPtr op2, bool is_init)
    : AssignExpr(std::move(op1), std::move(op2), is_init) {}

ValPtr IndexSliceAssignExpr::Eval(Frame* f) const {
    if ( is_init ) {
        RuntimeError("illegal assignment in initialization");
        return nullptr;
    }

    if ( auto v = op2->Eval(f) )
        op1->Assign(f, std::move(v));

    return nullptr;
}

IndexExpr::IndexExpr(ExprPtr arg_op1, ListExprPtr arg_op2, bool arg_is_slice, bool arg_is_inside_when)
    : BinaryExpr(EXPR_INDEX, std::move(arg_op1), std::move(arg_op2)),
      is_slice(arg_is_slice),
      is_inside_when(arg_is_inside_when) {
    if ( IsError() )
        return;

    if ( is_slice ) {
        if ( ! IsString(op1->GetType()->Tag()) && ! IsVector(op1->GetType()->Tag()) )
            ExprError("slice notation indexing only supported for strings and vectors currently");
    }

    else if ( IsString(op1->GetType()->Tag()) ) {
        if ( op2->AsListExpr()->Exprs().length() != 1 )
            ExprError("invalid string index expression");
    }

    if ( IsError() )
        return;

    if ( op1->GetType()->Tag() == TYPE_TABLE ) { // Check for a table[pattern] being indexed by a string
        const auto& table_type = op1->GetType()->AsTableType();
        const auto& rhs_type = op2->GetType()->AsTypeList()->GetTypes();
        if ( table_type->IsPatternIndex() && table_type->Yield() && rhs_type.size() == 1 &&
             IsString(rhs_type[0]->Tag()) ) {
            is_pattern_table = true;
            SetType(make_intrusive<VectorType>(op1->GetType()->Yield()));
            return;
        }
    }

    int match_type = op1->GetType()->MatchesIndex(op2->AsListExpr());

    if ( match_type == DOES_NOT_MATCH_INDEX ) {
        std::string error_msg =
            util::fmt("expression with type '%s' is not a type that can be indexed", type_name(op1->GetType()->Tag()));
        SetError(error_msg.data());
    }

    else if ( ! op1->GetType()->Yield() ) {
        if ( IsString(op1->GetType()->Tag()) && match_type == MATCHES_INDEX_SCALAR )
            SetType(base_type(TYPE_STRING));
        else
            // It's a set - so indexing it yields void.  We don't
            // directly generate an error message, though, since this
            // expression might be part of an add/delete statement,
            // rather than yielding a value.
            SetType(base_type(TYPE_VOID));
    }

    else if ( match_type == MATCHES_INDEX_SCALAR )
        SetType(op1->GetType()->Yield());

    else if ( match_type == MATCHES_INDEX_VECTOR )
        SetType(make_intrusive<VectorType>(op1->GetType()->Yield()));

    else
        ExprError("Unknown MatchesIndex() return value");
}

bool IndexExpr::CanAdd() const {
    if ( IsError() )
        return true; // avoid cascading the error report

    // "add" only allowed if our type is "set".
    return op1->GetType()->IsSet();
}

bool IndexExpr::CanDel() const {
    if ( IsError() )
        return true; // avoid cascading the error report

    return op1->GetType()->Tag() == TYPE_TABLE;
}

ValPtr IndexExpr::Add(Frame* f) {
    if ( IsError() )
        return nullptr;

    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    bool iterators_invalidated = false;
    v1->AsTableVal()->Assign(std::move(v2), nullptr, true, &iterators_invalidated);

    if ( iterators_invalidated )
        reporter->ExprRuntimeWarning(this, "possible loop/iterator invalidation");

    // In the future we could return a value, such as v1, here.
    return nullptr;
}

ValPtr IndexExpr::Delete(Frame* f) {
    if ( IsError() )
        return nullptr;

    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    bool iterators_invalidated = false;
    auto removed = v1->AsTableVal()->Remove(*v2, true, &iterators_invalidated);

    if ( iterators_invalidated )
        reporter->ExprRuntimeWarning(this, "possible loop/iterator invalidation");

    // In the future we could return a value, such as removed, here.
    return nullptr;
}

ExprPtr IndexExpr::MakeLvalue() {
    if ( IsString(op1->GetType()->Tag()) )
        ExprError("cannot assign to string index expression");

    return with_location_of(make_intrusive<RefExpr>(ThisPtr()), this);
}

ValPtr IndexExpr::Eval(Frame* f) const {
    auto v1 = op1->Eval(f);

    if ( ! v1 )
        return nullptr;

    auto v2 = op2->Eval(f);

    if ( ! v2 )
        return nullptr;

    Val* indv = v2->AsListVal()->Idx(0).get();

    if ( is_vector(v1) && is_vector(indv) ) {
        VectorVal* v_v1 = v1->AsVectorVal();
        VectorVal* v_v2 = indv->AsVectorVal();
        auto vt = cast_intrusive<VectorType>(GetType());

        // Booleans select each element (or not).
        if ( IsBool(v_v2->GetType()->Yield()->Tag()) ) {
            if ( v_v1->Size() != v_v2->Size() ) {
                RuntimeError("size mismatch, boolean index and vector");
                return nullptr;
            }

            return vector_bool_select(vt, v_v1, v_v2);
        }
        else
            // Elements are indices.
            return vector_int_select(vt, v_v1, v_v2);
    }
    else
        return Fold(v1.get(), v2.get());
}

ValPtr IndexExpr::Fold(Val* v1, Val* v2) const {
    if ( IsError() )
        return nullptr;

    ValPtr v;

    switch ( v1->GetType()->Tag() ) {
        case TYPE_VECTOR: {
            VectorVal* vect = v1->AsVectorVal();
            const ListVal* lv = v2->AsListVal();

            if ( lv->Length() == 1 ) {
                auto index = lv->Idx(0)->CoerceToInt();
                if ( index < 0 )
                    index = vect->Size() + index;

                v = vect->ValAt(index);
            }
            else
                return index_slice(vect, lv);
        } break;

        case TYPE_TABLE:
            if ( is_pattern_table )
                return v1->AsTableVal()->LookupPattern({NewRef{}, v2->AsListVal()->Idx(0)->AsStringVal()});

            v = v1->AsTableVal()->FindOrDefault({NewRef{}, v2});
            break;

        case TYPE_STRING: return index_string(v1->AsString(), v2->AsListVal());

        default: RuntimeError("type cannot be indexed"); break;
    }

    if ( v )
        return v;

    RuntimeError("no such index");
    return nullptr;
}

StringValPtr index_string(const String* s, const ListVal* lv) {
    int len = s->Len();
    String* substring = nullptr;

    if ( lv->Length() == 1 ) {
        zeek_int_t idx = lv->Idx(0)->AsInt();

        if ( idx < 0 )
            idx += len;

        // Out-of-range index will return null pointer.
        substring = s->GetSubstring(idx, 1);
    }
    else {
        zeek_int_t first = get_slice_index(lv->Idx(0)->AsInt(), len);
        zeek_int_t last = get_slice_index(lv->Idx(1)->AsInt(), len);
        zeek_int_t substring_len = last - first;

        if ( substring_len < 0 )
            substring = nullptr;
        else
            substring = s->GetSubstring(first, substring_len);
    }

    return make_intrusive<StringVal>(substring ? substring : new String(""));
}

VectorValPtr index_slice(VectorVal* vect, const ListVal* lv) {
    auto first = lv->Idx(0)->CoerceToInt();
    auto last = lv->Idx(1)->CoerceToInt();
    return index_slice(vect, first, last);
}

VectorValPtr index_slice(VectorVal* vect, int _first, int _last) {
    size_t len = vect->Size();
    auto result = make_intrusive<VectorVal>(vect->GetType<VectorType>());

    zeek_int_t first = get_slice_index(_first, len);
    zeek_int_t last = get_slice_index(_last, len);
    zeek_int_t sub_length = last - first;

    if ( sub_length >= 0 ) {
        result->Resize(sub_length);

        for ( zeek_int_t idx = first; idx < last; idx++ )
            result->Assign(idx - first, vect->ValAt(idx));
    }

    return result;
}

VectorValPtr vector_bool_select(VectorTypePtr vt, const VectorVal* v1, const VectorVal* v2) {
    auto v_result = make_intrusive<VectorVal>(std::move(vt));

    for ( unsigned int i = 0; i < v2->Size(); ++i )
        if ( v2->BoolAt(i) )
            v_result->Assign(v_result->Size() + 1, v1->ValAt(i));

    return v_result;
}

VectorValPtr vector_int_select(VectorTypePtr vt, const VectorVal* v1, const VectorVal* v2) {
    auto v_result = make_intrusive<VectorVal>(std::move(vt));

    // The elements are indices.
    //
    // ### Should handle negative indices here like S does, i.e.,
    // by excluding those elements.  Probably only do this if *all*
    // are negative.
    v_result->Resize(v2->Size());
    for ( unsigned int i = 0; i < v2->Size(); ++i ) {
        if ( auto idx_val = v2->ValAt(i) )
            v_result->Assign(i, v1->ValAt(idx_val->CoerceToInt()));
    }

    return v_result;
}

void IndexExpr::Assign(Frame* f, ValPtr v) {
    if ( IsError() )
        return;

    auto v1 = op1->Eval(f);
    auto v2 = op2->Eval(f);

    AssignToIndex(v1, v2, v);
}

void IndexExpr::ExprDescribe(ODesc* d) const {
    op1->Describe(d);
    if ( d->IsReadable() )
        d->Add("[");

    op2->Describe(d);
    if ( d->IsReadable() )
        d->Add("]");
}

static void report_field_deprecation(const RecordType* rt, const Expr* e, int field, bool has_check = false) {
    reporter->Deprecation(util::fmt("%s (%s)", rt->GetFieldDeprecationWarning(field, has_check).c_str(),
                                    obj_desc_short(e).c_str()),
                          e->GetLocationInfo());
}

FieldExpr::FieldExpr(ExprPtr arg_op, const char* arg_field_name)
    : UnaryExpr(EXPR_FIELD, std::move(arg_op)), field_name(util::copy_string(arg_field_name)) {
    if ( IsError() )
        return;

    if ( ! IsRecord(op->GetType()->Tag()) )
        ExprError("not a record");
    else {
        RecordType* rt = op->GetType()->AsRecordType();
        field = rt->FieldOffset(field_name);

        if ( field < 0 )
            ExprError("no such field in record");
        else {
            SetType(rt->GetFieldType(field));
            td = rt->FieldDecl(field);

            if ( rt->IsFieldDeprecated(field) )
                report_field_deprecation(rt, this, field);
        }
    }
}

FieldExpr::~FieldExpr() { delete[] field_name; }

ExprPtr FieldExpr::MakeLvalue() { return with_location_of(make_intrusive<RefExpr>(ThisPtr()), this); }

bool FieldExpr::CanDel() const { return td->GetAttr(ATTR_DEFAULT) || td->GetAttr(ATTR_OPTIONAL); }

void FieldExpr::Assign(Frame* f, ValPtr v) {
    if ( IsError() )
        return;

    Assign(op->Eval(f), std::move(v));
}

void FieldExpr::Assign(ValPtr lhs, ValPtr rhs) {
    if ( lhs )
        lhs->AsRecordVal()->Assign(field, std::move(rhs));
}

ValPtr FieldExpr::Delete(Frame* f) {
    auto op_v = op->Eval(f);
    if ( ! op_v )
        return nullptr;

    auto former = op_v->AsRecordVal()->GetField(field);
    Assign(std::move(op_v), nullptr);
    // In the future we could return a value, such as former, here.
    return nullptr;
}

ValPtr FieldExpr::Fold(Val* v) const {
    if ( const auto& result = v->AsRecordVal()->GetField(field) )
        return result;

    // Check for &default.
    const Attr* def_attr = td ? td->GetAttr(ATTR_DEFAULT).get() : nullptr;

    if ( def_attr )
        return eval_in_isolation(def_attr->GetExpr());
    else {
        RuntimeError("field value missing");
        assert(false);
        return nullptr; // Will never get here, but compiler can't tell.
    }
}

void FieldExpr::ExprDescribe(ODesc* d) const {
    op->Describe(d);
    if ( d->IsReadable() )
        d->Add("$");

    if ( IsError() )
        d->Add("<error>");
    else if ( d->IsReadable() )
        d->Add(field_name);
    else
        d->Add(field);
}

HasFieldExpr::HasFieldExpr(ExprPtr arg_op, const char* arg_field_name)
    : UnaryExpr(EXPR_HAS_FIELD, std::move(arg_op)), field_name(arg_field_name) {
    if ( IsError() )
        return;

    if ( ! IsRecord(op->GetType()->Tag()) )
        ExprError("not a record");
    else {
        RecordType* rt = op->GetType()->AsRecordType();
        field = rt->FieldOffset(field_name);

        if ( field < 0 )
            ExprError("no such field in record");
        else if ( rt->IsFieldDeprecated(field) )
            report_field_deprecation(rt, this, field, true);

        SetType(base_type(TYPE_BOOL));
    }
}

HasFieldExpr::~HasFieldExpr() { delete[] field_name; }

ValPtr HasFieldExpr::Fold(Val* v) const {
    auto rv = v->AsRecordVal();
    return val_mgr->Bool(rv->HasField(field));
}

void HasFieldExpr::ExprDescribe(ODesc* d) const {
    op->Describe(d);

    if ( d->IsReadable() )
        d->Add("?$");

    if ( IsError() )
        d->Add("<error>");
    else if ( d->IsReadable() )
        d->Add(field_name);
    else
        d->Add(field);
}

RecordConstructorExpr::RecordConstructorExpr(ListExprPtr constructor_list)
    : Expr(EXPR_RECORD_CONSTRUCTOR), op(std::move(constructor_list)), map(std::nullopt) {
    if ( IsError() )
        return;

    // Spin through the list, which should be comprised only of
    // record-field-assign expressions, and build up a
    // record type to associate with this constructor.
    const ExprPList& exprs = op->AsListExpr()->Exprs();
    type_decl_list* record_types = new type_decl_list(exprs.length());

    const Expr* constructor_error_expr = nullptr;

    for ( const auto& e : exprs ) {
        if ( e->Tag() != EXPR_FIELD_ASSIGN ) {
            // Don't generate the error yet, as reporting it
            // requires that we have a well-formed type.
            constructor_error_expr = e;
            SetError();
            continue;
        }

        FieldAssignExpr* field = static_cast<FieldAssignExpr*>(e);
        const auto& field_type = field->GetType();
        char* field_name = util::copy_string(field->FieldName());
        record_types->push_back(new TypeDecl(field_name, field_type));
    }

    SetType(make_intrusive<RecordType>(record_types));

    if ( constructor_error_expr )
        Error("bad type in record constructor", constructor_error_expr);
}

RecordConstructorExpr::RecordConstructorExpr(RecordTypePtr known_rt, ListExprPtr constructor_list,
                                             bool check_mandatory_fields)
    : Expr(EXPR_RECORD_CONSTRUCTOR), op(std::move(constructor_list)) {
    if ( IsError() )
        return;

    SetType(known_rt);

    const auto& exprs = op->AsListExpr()->Exprs();
    map = std::vector<int>(exprs.length());

    std::set<int> fields_seen; // used to check for missing fields

    int i = 0;
    for ( const auto& e : exprs ) {
        if ( e->Tag() != EXPR_FIELD_ASSIGN ) {
            Error("bad type in record constructor", e);
            SetError();
            continue;
        }

        auto field = e->AsFieldAssignExpr();
        int index = known_rt->FieldOffset(field->FieldName());

        if ( index < 0 ) {
            Error("no such field in record", e);
            SetError();
            continue;
        }

        auto known_ft = known_rt->GetFieldType(index);

        if ( ! field->PromoteTo(known_ft) )
            SetError();

        (*map)[i++] = index;
        fields_seen.insert(index);
    }

    if ( IsError() )
        return;

    if ( ! check_mandatory_fields )
        return;

    auto n = known_rt->NumFields();
    for ( i = 0; i < n; ++i )
        if ( ! fields_seen.contains(i) ) {
            const auto td_i = known_rt->FieldDecl(i);
            if ( IsAggr(td_i->type) )
                // These are always initialized.
                continue;

            if ( ! td_i->GetAttr(ATTR_OPTIONAL) ) {
                auto err = std::string("mandatory field \"") + known_rt->FieldName(i) + "\" missing";
                ExprError(err.c_str());
            }
        }
        else if ( known_rt->IsFieldDeprecated(i) )
            report_field_deprecation(known_rt.get(), this, i);
}

ValPtr RecordConstructorExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    const auto& exprs = op->Exprs();
    auto rt = cast_intrusive<RecordType>(type);

    if ( ! map && exprs.length() != rt->NumFields() )
        RuntimeErrorWithCallStack("inconsistency evaluating record constructor");

    auto rv = make_intrusive<RecordVal>(rt);

    for ( int i = 0; i < exprs.length(); ++i ) {
        auto v_i = exprs[i]->Eval(f);
        int ind = map ? (*map)[i] : i;

        if ( v_i && IsVector(v_i->GetType()->Tag()) && v_i->GetType<VectorType>()->IsUnspecifiedVector() ) {
            const auto& t_ind = rt->GetFieldType(ind);
            if ( IsVector(t_ind->Tag()) )
                v_i->AsVectorVal()->Concretize(t_ind->Yield());
        }

        rv->Assign(ind, v_i);
    }

    return rv;
}

bool RecordConstructorExpr::IsPure() const { return op->IsPure(); }

void RecordConstructorExpr::ExprDescribe(ODesc* d) const {
    auto& tn = type->GetName();

    if ( tn.empty() ) {
        d->Add("[");
        op->Describe(d);
        d->Add("]");
    }
    else {
        d->Add(tn);
        d->Add("(");
        op->Describe(d);
        d->Add(")");
    }
}

TraversalCode RecordConstructorExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

static ExprPtr expand_one_elem(const ExprPList& index_exprs, ExprPtr yield, ExprPtr elem, int elem_offset) {
    auto expanded_elem = with_location_of(make_intrusive<ListExpr>(), elem);

    for ( int i = 0; i < index_exprs.length(); ++i )
        if ( i == elem_offset )
            expanded_elem->Append(elem);
        else
            expanded_elem->Append({NewRef{}, index_exprs[i]});

    if ( yield )
        return with_location_of(make_intrusive<AssignExpr>(expanded_elem, yield, true), elem);
    else
        return expanded_elem;
}

static bool expand_op_elem(ListExprPtr elems, ExprPtr elem, TypePtr t) {
    ExprPtr index;
    ExprPtr yield;

    if ( elem->Tag() == EXPR_ASSIGN ) {
        if ( t ) {
            if ( ! t->IsTable() ) {
                elem->Error("table constructor used in a non-table context");
                return false;
            }

            t = t->AsTableType()->GetIndices();
        }

        index = elem->GetOp1();
        yield = elem->GetOp2();
    }
    else
        index = elem; // this is a set - no yield

    // If the index isn't a list, then there's nothing to consider
    // expanding.
    if ( index->Tag() != EXPR_LIST ) {
        elems->Append(elem);
        return false;
    }

    // Look inside the index for any sub-lists or sets, and expand those.
    // There might be more than one, but we'll pick that up recursively
    // later.
    auto& index_exprs = index->AsListExpr()->Exprs();
    int index_n = index_exprs.length();
    int list_offset = -1;
    int set_offset = -1;
    for ( int i = 0; i < index_n; ++i ) {
        auto& ie_i = index_exprs[i];

        if ( ie_i->Tag() == EXPR_LIST ) {
            list_offset = i;
            break;
        }

        if ( ie_i->GetType()->IsSet() ) {
            // Check for this set corresponding to what's expected
            // in this location, in which case it shouldn't be
            // expanded.
            const TypeList* tl = nullptr;
            if ( t && t->Tag() == TYPE_LIST )
                tl = t->AsTypeList();

            // So we're good-to-go in expanding if either
            // (1) we weren't given a type, or it's not a list,
            // or (2) it's a list, but doesn't correspond in
            // length to the list of expressions, or (3) it does
            // but its corresponding element at this position
            // doesn't have the same type as this set.
            if ( ! tl || static_cast<int>(tl->GetTypes().size()) != index_n ||
                 ! same_type(tl->GetTypes()[i], ie_i->GetType()) ) {
                set_offset = i;
                break;
            }
        }
    }

    if ( set_offset >= 0 ) { // expand the set
        auto s_e = index_exprs[set_offset];
        auto v = eval_in_isolation(s_e);
        if ( ! v ) {
            s_e->Error("cannot expand constructor elements using a value that depends on local variables");
            elems->SetError();
            return false;
        }

        for ( auto& s_elem : v->AsTableVal()->ToMap() ) {
            auto c_elem = with_location_of(make_intrusive<ConstExpr>(s_elem.first), elem);
            elems->Append(expand_one_elem(index_exprs, yield, c_elem, set_offset));
        }

        return true;
    }

    if ( list_offset < 0 ) { // No embedded lists.
        elems->Append(elem);
        return false;
    }

    // Expand the identified list.
    auto sub_list = index_exprs[list_offset]->AsListExpr();
    for ( auto& sub_list_i : sub_list->Exprs() ) {
        ExprPtr e = {NewRef{}, sub_list_i};
        elems->Append(expand_one_elem(index_exprs, yield, e, list_offset));
    }

    return true;
}

ListExprPtr expand_op(ListExprPtr op, const TypePtr& t) {
    auto new_list = with_location_of(make_intrusive<ListExpr>(), op);
    bool did_expansion = false;

    for ( auto e : op->Exprs() ) {
        if ( expand_op_elem(new_list, {NewRef{}, e}, t) )
            did_expansion = true;

        if ( new_list->IsError() ) {
            op->SetError();
            return op;
        }
    }

    if ( did_expansion )
        return expand_op(new_list, t);
    else
        return op;
}

TableConstructorExpr::TableConstructorExpr(ListExprPtr constructor_list,
                                           std::unique_ptr<std::vector<AttrPtr>> arg_attrs, TypePtr arg_type,
                                           AttributesPtr arg_attrs2)
    : UnaryExpr(EXPR_TABLE_CONSTRUCTOR, expand_op(std::move(constructor_list), arg_type)) {
    if ( IsError() )
        return;

    if ( arg_type ) {
        if ( ! arg_type->IsTable() ) {
            Error("bad table constructor type", arg_type.get());
            SetError();
            return;
        }

        SetType(std::move(arg_type));
    }
    else {
        if ( op->AsListExpr()->Exprs().empty() )
            SetType(make_intrusive<TableType>(make_intrusive<TypeList>(base_type(TYPE_ANY)), base_type(TYPE_ANY)));
        else {
            SetType(init_type(op));

            if ( ! type ) {
                SetError("cannot determine type for table; ensure square brackets are around each key");
                return;
            }

            else if ( type->Tag() != TYPE_TABLE || type->AsTableType()->IsSet() ) {
                SetError("values in table(...) constructor do not specify a table");
                return;
            }
        }
    }

    if ( arg_attrs )
        SetAttrs(make_intrusive<Attributes>(std::move(*arg_attrs), type, false, false));
    else
        SetAttrs(arg_attrs2);

    const auto& indices = type->AsTableType()->GetIndices()->GetTypes();
    const ExprPList& cle = op->AsListExpr()->Exprs();

    // check and promote all assign expressions in ctor list
    for ( const auto& expr : cle ) {
        if ( expr->Tag() != EXPR_ASSIGN ) {
            expr->Error("illegal table constructor element");
            SetError();
            return;
        }

        auto idx_expr = expr->AsAssignExpr()->GetOp1();
        auto val_expr = expr->AsAssignExpr()->GetOp2();
        auto yield_type = GetType()->AsTableType()->Yield();

        if ( idx_expr->Tag() != EXPR_LIST ) {
            expr->Error("table constructor index is not a list");
            SetError();
            return;
        }

        // Promote LHS
        ExprPList& idx_exprs = idx_expr->AsListExpr()->Exprs();

        if ( idx_exprs.length() != static_cast<int>(indices.size()) )
            continue;

        loop_over_list(idx_exprs, j) {
            ExprPtr idx = {NewRef{}, idx_exprs[j]};

            auto promoted_idx = check_and_promote_expr(idx, indices[j]);

            if ( promoted_idx ) {
                if ( promoted_idx != idx )
                    Unref(idx_exprs.replace(j, promoted_idx.release()));

                continue;
            }

            ExprError("inconsistent types in table constructor");
            return;
        }

        // Promote RHS
        if ( auto promoted_val = check_and_promote_expr(val_expr, yield_type) ) {
            if ( promoted_val != val_expr )
                expr->AsAssignExpr()->SetOp2(promoted_val);
        }
        else {
            ExprError("inconsistent types in table constructor");
            return;
        }
    }
}

TraversalCode TableConstructorExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    if ( attrs ) {
        tc = attrs->Traverse(cb);
        HANDLE_TC_EXPR_PRE(tc);
    }

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

ValPtr TableConstructorExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto tv = make_intrusive<TableVal>(GetType<TableType>(), attrs);
    const ExprPList& exprs = op->AsListExpr()->Exprs();

    for ( const auto& expr : exprs ) {
        auto op1 = expr->GetOp1();
        auto op2 = expr->GetOp2();

        if ( ! op1 || ! op2 )
            return nullptr;

        auto index = op1->Eval(f);
        auto v = op2->Eval(f);

        if ( ! index || ! v )
            return nullptr;

        if ( ! tv->Assign(std::move(index), std::move(v)) )
            RuntimeError("type clash in table assignment");
    }

    tv->InitDefaultFunc(f);

    return tv;
}

void TableConstructorExpr::ExprDescribe(ODesc* d) const {
    d->Add("table(");
    op->Describe(d);
    d->Add(")");

    if ( attrs )
        attrs->Describe(d);
}

SetConstructorExpr::SetConstructorExpr(ListExprPtr constructor_list, std::unique_ptr<std::vector<AttrPtr>> arg_attrs,
                                       TypePtr arg_type, AttributesPtr arg_attrs2)
    : UnaryExpr(EXPR_SET_CONSTRUCTOR, expand_op(std::move(constructor_list), arg_type)) {
    if ( IsError() )
        return;

    if ( arg_type ) {
        if ( ! arg_type->IsSet() ) {
            Error("bad set constructor type", arg_type.get());
            SetError();
            return;
        }

        SetType(std::move(arg_type));
    }
    else {
        if ( op->AsListExpr()->Exprs().empty() )
            SetType(make_intrusive<TableType>(make_intrusive<TypeList>(base_type(TYPE_ANY)), nullptr));
        else
            SetType(init_type(op));
    }

    if ( ! type )
        SetError();

    else if ( type->Tag() != TYPE_TABLE || ! type->AsTableType()->IsSet() )
        SetError("values in set(...) constructor do not specify a set");

    if ( type->Tag() == TYPE_ERROR )
        return;

    if ( arg_attrs )
        SetAttrs(make_intrusive<Attributes>(std::move(*arg_attrs), type, false, false));
    else
        SetAttrs(std::move(arg_attrs2));

    const auto& indices = type->AsTableType()->GetIndices()->GetTypes();
    ExprPList& cle = op->AsListExpr()->Exprs();

    if ( indices.size() == 1 ) {
        if ( ! check_and_promote_exprs_to_type(op->AsListExpr(), indices[0]) )
            ExprError("inconsistent type in set constructor");
    }

    else if ( indices.size() > 1 ) {
        // Check/promote each expression in composite index.
        loop_over_list(cle, i) {
            Expr* ce = cle[i];

            if ( ce->Tag() != EXPR_LIST ) {
                ce->Error("not a list of indices");
                SetError();
                return;
            }

            ListExpr* le = ce->AsListExpr();

            if ( check_and_promote_exprs(le, type->AsTableType()->GetIndices()) ) {
                if ( le != cle[i] )
                    cle.replace(i, le);

                continue;
            }

            ExprError("inconsistent types in set constructor");
        }
    }
}

TraversalCode SetConstructorExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = op->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    if ( attrs ) {
        tc = attrs->Traverse(cb);
        HANDLE_TC_EXPR_PRE(tc);
    }

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

ValPtr SetConstructorExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto aggr = make_intrusive<TableVal>(IntrusivePtr{NewRef{}, type->AsTableType()}, attrs);
    const ExprPList& exprs = op->AsListExpr()->Exprs();

    for ( const auto& expr : exprs ) {
        auto element = expr->Eval(f);
        aggr->Assign(std::move(element), nullptr);
    }

    return aggr;
}

void SetConstructorExpr::ExprDescribe(ODesc* d) const {
    d->Add("set(");
    op->Describe(d);
    d->Add(")");

    if ( attrs )
        attrs->Describe(d);
}

VectorConstructorExpr::VectorConstructorExpr(ListExprPtr constructor_list, TypePtr arg_type)
    : UnaryExpr(EXPR_VECTOR_CONSTRUCTOR, std::move(constructor_list)) {
    if ( IsError() )
        return;

    if ( arg_type ) {
        if ( arg_type->Tag() != TYPE_VECTOR ) {
            Error("bad vector constructor type", arg_type.get());
            SetError();
            return;
        }

        SetType(std::move(arg_type));
    }
    else {
        if ( op->AsListExpr()->Exprs().empty() ) {
            // vector().
            // By default, assign VOID type here. A vector with
            // void type set is seen as an unspecified vector.
            SetType(make_intrusive<VectorType>(base_type(TYPE_VOID)));
            return;
        }

        if ( auto t = maximal_type(op->AsListExpr()) )
            SetType(make_intrusive<VectorType>(std::move(t)));
        else {
            SetError();
            return;
        }
    }

    if ( ! check_and_promote_exprs_to_type(op->AsListExpr(), type->AsVectorType()->Yield()) )
        ExprError("inconsistent types in vector constructor");
}

ValPtr VectorConstructorExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto vec = make_intrusive<VectorVal>(GetType<VectorType>());
    const ExprPList& exprs = op->AsListExpr()->Exprs();

    loop_over_list(exprs, i) {
        Expr* e = exprs[i];

        if ( ! vec->Assign(i, e->Eval(f)) ) {
            RuntimeError(util::fmt("type mismatch at index %d", i));
            return nullptr;
        }
    }

    return vec;
}

void VectorConstructorExpr::ExprDescribe(ODesc* d) const {
    d->Add("vector(");
    op->Describe(d);
    d->Add(")");
}

FieldAssignExpr::FieldAssignExpr(const char* arg_field_name, ExprPtr value)
    : UnaryExpr(EXPR_FIELD_ASSIGN, std::move(value)), field_name(arg_field_name) {
    SetType(op->GetType());
}

bool FieldAssignExpr::PromoteTo(TypePtr t) {
    op = check_and_promote_expr(op, t);
    return op != nullptr;
}

bool FieldAssignExpr::IsRecordElement(TypeDecl* td) const {
    if ( td ) {
        td->type = op->GetType();
        td->id = util::copy_string(field_name.c_str(), field_name.size());
    }

    return true;
}

void FieldAssignExpr::ExprDescribe(ODesc* d) const {
    d->Add("$");
    d->Add(FieldName());
    d->Add("=");

    if ( op )
        op->Describe(d);
    else
        d->Add("<error>");
}

ArithCoerceExpr::ArithCoerceExpr(ExprPtr arg_op, TypeTag t) : UnaryExpr(EXPR_ARITH_COERCE, std::move(arg_op)) {
    if ( IsError() )
        return;

    TypeTag bt = op->GetType()->Tag();
    TypeTag vbt = bt;

    if ( IsVector(bt) ) {
        SetType(make_intrusive<VectorType>(base_type(t)));
        vbt = op->GetType()->AsVectorType()->Yield()->Tag();
    }
    else
        SetType(base_type(t));

    if ( (bt == TYPE_ENUM) != (t == TYPE_ENUM) )
        ExprError("can't convert to/from enumerated type");

    else if ( ! IsArithmetic(t) && ! IsBool(t) && t != TYPE_TIME && t != TYPE_INTERVAL )
        ExprError("bad coercion");

    else if ( ! IsArithmetic(bt) && ! IsBool(bt) && ! IsArithmetic(vbt) && ! IsBool(vbt) )
        ExprError("bad coercion value");
}

ValPtr ArithCoerceExpr::FoldSingleVal(ValPtr v, const TypePtr& t) const {
    return check_and_promote(v, t, false, location);
}

ValPtr ArithCoerceExpr::Fold(Val* v) const {
    auto t = GetType();

    if ( ! is_vector(v) ) {
        // Our result type might be vector, in which case this
        // invocation is being done per-element rather than on
        // the whole vector.  Correct the type if so.
        if ( type->Tag() == TYPE_VECTOR )
            t = t->AsVectorType()->Yield();

        return FoldSingleVal({NewRef{}, v}, t);
    }

    VectorVal* vv = v->AsVectorVal();
    auto result = make_intrusive<VectorVal>(cast_intrusive<VectorType>(t));

    auto yt = t->AsVectorType()->Yield();

    for ( unsigned int i = 0; i < vv->Size(); ++i ) {
        auto elt = vv->ValAt(i);
        if ( elt )
            result->Assign(i, FoldSingleVal(elt, yt));
        else
            result->Assign(i, nullptr);
    }

    return result;
}

// Returns true if the record type or any of its fields have an error.
static bool record_type_has_errors(const RecordType* rt) {
    if ( IsErrorType(rt->Tag()) )
        return true;

    if ( rt->NumFields() > 0 )
        for ( const auto* td : *rt->Types() )
            if ( IsErrorType(td->type->Tag()) )
                return true;

    return false;
}

RecordCoerceExpr::RecordCoerceExpr(ExprPtr arg_op, RecordTypePtr r) : UnaryExpr(EXPR_RECORD_COERCE, std::move(arg_op)) {
    if ( IsError() )
        return;

    SetType(std::move(r));

    if ( GetType()->Tag() != TYPE_RECORD )
        ExprError("coercion to non-record");

    else if ( op->GetType()->Tag() != TYPE_RECORD )
        ExprError("coercion of non-record to record");

    else {
        RecordType* t_r = type->AsRecordType();
        RecordType* sub_r = op->GetType()->AsRecordType();

        if ( record_type_has_errors(t_r) || record_type_has_errors(sub_r) ) {
            SetError();
            return;
        }

        int map_size = t_r->NumFields();
        map.resize(map_size, -1); // -1 = field is not mapped

        int i;
        for ( i = 0; i < sub_r->NumFields(); ++i ) {
            int t_i = t_r->FieldOffset(sub_r->FieldName(i));
            if ( t_i < 0 ) {
                ExprError(util::fmt("orphaned field \"%s\" in record coercion", sub_r->FieldName(i)));
                break;
            }

            const auto& sub_t_i = sub_r->GetFieldType(i);
            const auto& sup_t_i = t_r->GetFieldType(t_i);

            if ( ! same_type(sup_t_i, sub_t_i) ) {
                auto is_arithmetic_promotable = [](zeek::Type* sup, zeek::Type* sub) -> bool {
                    auto sup_tag = sup->Tag();
                    auto sub_tag = sub->Tag();

                    if ( ! BothArithmetic(sup_tag, sub_tag) )
                        return false;

                    if ( sub_tag == TYPE_DOUBLE && IsIntegral(sup_tag) )
                        return false;

                    if ( sub_tag == TYPE_INT && sup_tag == TYPE_COUNT )
                        return false;

                    return true;
                };

                auto is_record_promotable = [](zeek::Type* sup, zeek::Type* sub) -> bool {
                    if ( sup->Tag() != TYPE_RECORD )
                        return false;

                    if ( sub->Tag() != TYPE_RECORD )
                        return false;

                    return record_promotion_compatible(sup->AsRecordType(), sub->AsRecordType());
                };

                if ( ! is_arithmetic_promotable(sup_t_i.get(), sub_t_i.get()) &&
                     ! is_record_promotable(sup_t_i.get(), sub_t_i.get()) ) {
                    std::string error_msg = util::fmt("type clash for field \"%s\"", sub_r->FieldName(i));
                    Error(error_msg.c_str(), sub_t_i.get());
                    SetError();
                    break;
                }
            }

            map[t_i] = i;
        }

        if ( IsError() )
            return;

        for ( i = 0; i < map_size; ++i ) {
            if ( map[i] == -1 ) {
                if ( ! t_r->FieldDecl(i)->GetAttr(ATTR_OPTIONAL) ) {
                    std::string error_msg = util::fmt("non-optional field \"%s\" missing", t_r->FieldName(i));
                    Error(error_msg.c_str());
                    SetError();
                    break;
                }
            }
            else if ( t_r->IsFieldDeprecated(i) )
                report_field_deprecation(t_r, this, i);
        }
    }
}

ValPtr RecordCoerceExpr::Fold(Val* v) const {
    if ( same_type(GetType(), Op()->GetType()) )
        return IntrusivePtr{NewRef{}, v};

    auto rt = cast_intrusive<RecordType>(GetType());
    return coerce_to_record(rt, v, map);
}

RecordValPtr coerce_to_record(RecordTypePtr rt, Val* v, const std::vector<int>& map) {
    int map_size = map.size();
    auto val = make_intrusive<RecordVal>(rt);
    RecordType* val_type = val->GetType()->AsRecordType();

    RecordVal* rv = v->AsRecordVal();

    for ( int i = 0; i < map_size; ++i ) {
        if ( map[i] >= 0 ) {
            auto rhs = rv->GetField(map[i]);

            if ( ! rhs ) {
                auto rv_rt = rv->GetType()->AsRecordType();
                const auto& def = rv_rt->FieldDecl(map[i])->GetAttr(ATTR_DEFAULT);

                if ( def )
                    rhs = eval_in_isolation(def->GetExpr());
            }

            assert(rhs || rt->FieldDecl(i)->GetAttr(ATTR_OPTIONAL));

            if ( ! rhs ) {
                // Optional field is missing.
                val->Remove(i);
                continue;
            }

            const auto& rhs_type = rhs->GetType();
            const auto& field_type = val_type->GetFieldType(i);

            if ( rhs_type->Tag() == TYPE_RECORD && field_type->Tag() == TYPE_RECORD &&
                 ! same_type(rhs_type, field_type) ) {
                if ( auto new_val = rhs->AsRecordVal()->CoerceTo(cast_intrusive<RecordType>(field_type)) )
                    rhs = std::move(new_val);
            }
            else if ( rhs_type->Tag() == TYPE_VECTOR && field_type->Tag() == TYPE_VECTOR &&
                      rhs_type->AsVectorType()->IsUnspecifiedVector() ) {
                auto rhs_v = rhs->AsVectorVal();
                if ( ! rhs_v->Concretize(field_type->Yield()) )
                    reporter->InternalError("could not concretize empty vector");
            }
            else if ( BothArithmetic(rhs_type->Tag(), field_type->Tag()) && ! same_type(rhs_type, field_type) ) {
                auto new_val = check_and_promote(rhs, field_type, false);
                rhs = std::move(new_val);
            }

            val->Assign(i, std::move(rhs));
        }
        else {
            if ( const auto& def = rt->FieldDecl(i)->GetAttr(ATTR_DEFAULT) ) {
                auto def_val = eval_in_isolation(def->GetExpr());
                const auto& def_type = def_val->GetType();
                const auto& field_type = rt->GetFieldType(i);

                if ( def_type->Tag() == TYPE_RECORD && field_type->Tag() == TYPE_RECORD &&
                     ! same_type(def_type, field_type) ) {
                    auto tmp = def_val->AsRecordVal()->CoerceTo(cast_intrusive<RecordType>(field_type));

                    if ( tmp )
                        def_val = std::move(tmp);
                }

                val->Assign(i, std::move(def_val));
            }
            else
                val->Remove(i);
        }
    }

    return val;
}

TableCoerceExpr::TableCoerceExpr(ExprPtr arg_op, TableTypePtr tt, bool type_check)
    : UnaryExpr(EXPR_TABLE_COERCE, std::move(arg_op)) {
    if ( IsError() )
        return;

    if ( type_check ) {
        op = check_and_promote_expr(op, tt);
        if ( ! op ) {
            SetError();
            return;
        }

        if ( op->Tag() == EXPR_TABLE_COERCE && op->GetType() == tt )
            // Avoid double-coercion.
            op = op->GetOp1();
    }

    SetType(std::move(tt));

    if ( GetType()->Tag() != TYPE_TABLE )
        ExprError("coercion to non-table");

    else if ( op->GetType()->Tag() != TYPE_TABLE )
        ExprError("coercion of non-table/set to table/set");
}

ValPtr TableCoerceExpr::Fold(Val* v) const {
    TableVal* tv = v->AsTableVal();

    if ( tv->Size() > 0 )
        RuntimeErrorWithCallStack("coercion of non-empty table/set");

    return make_intrusive<TableVal>(GetType<TableType>(), tv->GetAttrs());
}

VectorCoerceExpr::VectorCoerceExpr(ExprPtr arg_op, VectorTypePtr v) : UnaryExpr(EXPR_VECTOR_COERCE, std::move(arg_op)) {
    if ( IsError() )
        return;

    SetType(std::move(v));

    if ( GetType()->Tag() != TYPE_VECTOR )
        ExprError("coercion to non-vector");

    else if ( op->GetType()->Tag() != TYPE_VECTOR )
        ExprError("coercion of non-vector to vector");
}

ValPtr VectorCoerceExpr::Fold(Val* v) const {
    VectorVal* vv = v->AsVectorVal();

    if ( vv->Size() > 0 )
        RuntimeErrorWithCallStack("coercion of non-empty vector");

    return make_intrusive<VectorVal>(GetType<VectorType>());
}

ScheduleTimer::ScheduleTimer(const EventHandlerPtr& arg_event, Args arg_args, double t)
    : Timer(t, TIMER_SCHEDULE), event(arg_event), args(std::move(arg_args)) {}

void ScheduleTimer::Dispatch(double /* t */, bool /* is_expire */) {
    if ( event ) {
        detail::EventMetadataVectorPtr meta;

        if ( BifConst::EventMetadata::add_network_timestamp ) {
            // An event's intended timestamp might be in the past as timer expiration is driven by
            // network time. Guarantee that the intended timestamp is never in the future (e.g.,
            // when all timers are expired on shutdown).
            auto ts = std::min(this->Time(), run_state::network_time);
            meta = detail::MakeEventMetadataVector(ts);
        }

        event_mgr.Enqueue(std::move(meta), event, std::move(args));
    }
}

ScheduleExpr::ScheduleExpr(ExprPtr arg_when, EventExprPtr arg_event)
    : Expr(EXPR_SCHEDULE), when(std::move(arg_when)), event(std::move(arg_event)) {
    if ( IsError() || when->IsError() || event->IsError() )
        return;

    TypeTag bt = when->GetType()->Tag();

    if ( bt != TYPE_TIME && bt != TYPE_INTERVAL )
        ExprError("schedule expression requires a time or time interval");
}

ValPtr ScheduleExpr::Eval(Frame* f) const {
    if ( run_state::terminating )
        return nullptr;

    auto when_val = when->Eval(f);

    if ( ! when_val )
        return nullptr;

    double dt = when_val->InternalDouble();

    if ( when->GetType()->Tag() == TYPE_INTERVAL )
        dt += run_state::network_time;

    auto args = eval_list(f, event->Args());

    if ( args ) {
        auto handler = event->Handler();

        if ( event_trace_mgr )
            event_trace_mgr->ScriptEventQueued(handler);

        timer_mgr->Add(new ScheduleTimer(handler, std::move(*args), dt));
    }

    return nullptr;
}

TraversalCode ScheduleExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = when->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = event->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

void ScheduleExpr::ExprDescribe(ODesc* d) const {
    if ( d->IsReadable() )
        d->AddSP("schedule");

    when->Describe(d);
    d->SP();

    if ( d->IsReadable() ) {
        d->Add("{");
        d->PushIndent();
        event->Describe(d);
        d->PopIndent();
        d->Add("}");
    }
    else
        event->Describe(d);
}

InExpr::InExpr(ExprPtr arg_op1, ExprPtr arg_op2) : BinaryExpr(EXPR_IN, std::move(arg_op1), std::move(arg_op2)) {
    if ( IsError() )
        return;

    if ( op1->GetType()->Tag() == TYPE_PATTERN ) {
        if ( op2->GetType()->Tag() == TYPE_STRING ) {
            SetType(base_type(TYPE_BOOL));
            return;
        }
        else if ( op2->GetType()->Tag() == TYPE_TABLE ) {
            // fall through to type-checking at end of function
        }
        else {
            op2->GetType()->Error("pattern requires string or set/table index", op1.get());
            SetError();
            return;
        }
    }

    if ( op1->GetType()->Tag() == TYPE_STRING && op2->GetType()->Tag() == TYPE_STRING ) {
        SetType(base_type(TYPE_BOOL));
        return;
    }

    // Check for:	<addr> in <subnet>
    //		<addr> in set[subnet]
    //		<addr> in table[subnet] of ...
    if ( op1->GetType()->Tag() == TYPE_ADDR ) {
        if ( op2->GetType()->Tag() == TYPE_SUBNET ) {
            SetType(base_type(TYPE_BOOL));
            return;
        }

        if ( op2->GetType()->Tag() == TYPE_TABLE && op2->GetType()->AsTableType()->IsSubNetIndex() ) {
            SetType(base_type(TYPE_BOOL));
            return;
        }
    }

    // Support <string> in table[pattern] / set[pattern]
    if ( op1->GetType()->Tag() == TYPE_STRING ) {
        if ( op2->GetType()->Tag() == TYPE_TABLE ) {
            const auto& table_type = op2->GetType()->AsTableType();

            if ( table_type->IsPatternIndex() ) {
                SetType(base_type(TYPE_BOOL));
                return;
            }
        }
    }

    if ( op1->Tag() != EXPR_LIST )
        op1 = with_location_of(make_intrusive<ListExpr>(op1), op1);

    ListExpr* lop1 = op1->AsListExpr();

    if ( ! op2->GetType()->MatchesIndex(lop1) )
        SetError("not an index type");
    else
        SetType(base_type(TYPE_BOOL));
}

ValPtr InExpr::Fold(Val* v1, Val* v2) const {
    if ( v2->GetType()->Tag() == TYPE_STRING ) {
        const String* s2 = v2->AsString();

        if ( v1->GetType()->Tag() == TYPE_PATTERN ) {
            auto re = v1->As<PatternVal*>();
            return val_mgr->Bool(re->MatchAnywhere(s2) != 0);
        }

        const String* s1 = v1->AsString();

        // Could do better here e.g. Boyer-Moore if done repeatedly.
        auto s = reinterpret_cast<const unsigned char*>(s1->CheckString());
        auto res = util::strstr_n(s2->Len(), s2->Bytes(), s1->Len(), s) != -1;
        return val_mgr->Bool(res);
    }

    if ( v1->GetType()->Tag() == TYPE_ADDR && v2->GetType()->Tag() == TYPE_SUBNET )
        return val_mgr->Bool(v2->AsSubNetVal()->Contains(v1->AsAddr()));

    bool res;

    if ( is_vector(v2) ) {
        auto vv2 = v2->AsVectorVal();
        auto ind = v1->AsListVal()->Idx(0)->CoerceToUnsigned();
        res = ind < vv2->Size() && vv2->ValAt(ind);
    }
    else {
        const auto& table_val = v2->AsTableVal();
        const auto& table_type = table_val->GetType<zeek::TableType>();
        // Special table[pattern] / set[pattern] in expression.
        if ( table_type->IsPatternIndex() && v1->GetType()->Tag() == TYPE_STRING )
            res = table_val->MatchPattern({NewRef{}, v1->AsStringVal()});
        else
            res = static_cast<bool>(v2->AsTableVal()->Find({NewRef{}, v1}));
    }

    return val_mgr->Bool(res);
}

CallExpr::CallExpr(ExprPtr arg_func, ListExprPtr arg_args, bool in_hook, bool _in_when)
    : Expr(EXPR_CALL), func(std::move(arg_func)), args(std::move(arg_args)), in_when(_in_when) {
    if ( func->IsError() || args->IsError() ) {
        SetError();
        return;
    }

    const auto& func_type = func->GetType();

    if ( ! IsFunc(func_type->Tag()) ) {
        func->Error("not a function");
        SetError();
        return;
    }

    if ( func_type->AsFuncType()->Flavor() == FUNC_FLAVOR_HOOK && ! in_hook ) {
        func->Error("hook cannot be called directly, use hook operator");
        SetError();
        return;
    }

    if ( record_type_has_errors(func_type->AsFuncType()->Params()->AsRecordType()) )
        SetError();
    else if ( ! func_type->MatchesIndex(args.get()) )
        SetError("argument type mismatch in function call");
    else {
        const auto& yield = func_type->Yield();

        if ( ! yield ) {
            switch ( func_type->AsFuncType()->Flavor() ) {
                case FUNC_FLAVOR_FUNCTION:
                    Error("function has no yield type");
                    SetError();
                    break;

                case FUNC_FLAVOR_EVENT:
                    Error("event called in expression, use event statement instead");
                    SetError();
                    break;

                case FUNC_FLAVOR_HOOK:
                    Error("hook has no yield type");
                    SetError();
                    break;

                default:
                    Error("invalid function flavor");
                    SetError();
                    break;
            }
        }
        else
            SetType(yield);

        // Check for call to built-ins that can be statically analyzed.
        ValPtr func_val;

        if ( func->Tag() == EXPR_NAME &&
             // This is cheating, but without it processing gets
             // quite confused regarding "value used but not set"
             // run-time errors when we apply this analysis during
             // parsing.  Really we should instead do it after we've
             // parsed the entire set of scripts.
             util::streq((static_cast<NameExpr*>(func.get()))->Id()->Name(), "fmt") &&
             // The following is needed because fmt might not yet
             // be bound as a name.
             did_builtin_init ) {
            func_val = eval_in_isolation(func);
            if ( func_val ) {
                zeek::Func* f = func_val->AsFunc();
                if ( f->GetKind() == Func::BUILTIN_FUNC && ! check_built_in_call(static_cast<BuiltinFunc*>(f), this) )
                    SetError();
            }
        }
    }
}

bool CallExpr::IsPure() const {
    if ( IsError() )
        return true;

    if ( func->Tag() != EXPR_NAME )
        // Indirect call, can't resolve up front.
        return false;

    auto func_id = func->AsNameExpr()->Id();
    if ( ! func_id->IsGlobal() )
        return false;

    auto func_val = func_id->GetVal();
    if ( ! func_val )
        return false;

    zeek::Func* f = func_val->AsFunc();

    // Only recurse for built-in functions, as recursing on script
    // functions can lead to infinite recursion if the function being
    // called here happens to be recursive (either directly
    // or indirectly).
    bool pure = false;

    if ( f->GetKind() == Func::BUILTIN_FUNC )
        pure = f->IsPure() && args->IsPure();

    return pure;
}

ValPtr CallExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    // If we are inside a trigger condition, we may have already been
    // called, delayed, and then produced a result which is now cached.
    // Check for that.
    if ( f ) {
        if ( trigger::Trigger* trigger = f->GetTrigger() ) {
            if ( Val* v = trigger->Lookup(this) ) {
                DBG_LOG(DBG_NOTIFIERS, "%s: provides cached function result", trigger->Name());
                return {NewRef{}, v};
            }
        }
    }

    ValPtr ret;
    auto func_val = func->Eval(f);
    auto v = eval_list(f, args.get());

    if ( func_val && v ) {
        const zeek::Func* funcv = func_val->AsFunc();
        auto current_assoc = f ? f->GetTriggerAssoc() : nullptr;

        if ( f )
            f->SetCall(this);

        auto& args = *v;
        ret = funcv->Invoke(&args, f);

        if ( f )
            f->SetTriggerAssoc(current_assoc);
    }

    return ret;
}

TraversalCode CallExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = func->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = args->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

void CallExpr::ExprDescribe(ODesc* d) const {
    func->Describe(d);
    if ( d->IsReadable() ) {
        d->Add("(");
        args->Describe(d);
        d->Add(")");
    }
    else
        args->Describe(d);
}

LambdaExpr::LambdaExpr(FunctionIngredientsPtr arg_ing, IDPList arg_outer_ids, std::string name, StmtPtr when_parent)
    : Expr(EXPR_LAMBDA) {
    ingredients = std::move(arg_ing);
    outer_ids = std::move(arg_outer_ids);

    auto ingr_t = ingredients->GetID()->GetType<FuncType>();
    SetType(ingr_t);
    captures = ingr_t->GetCaptures();

    if ( ! CheckCaptures(std::move(when_parent)) ) {
        SetError();
        return;
    }

    // Now that we've validated that the captures match the outer_ids,
    // we regenerate the latter to come in the same order as the captures.
    // This avoids potentially subtle bugs when doing script optimization
    // where one context uses the outer_ids and another uses the captures.
    if ( captures ) {
        outer_ids.clear();
        for ( auto& c : *captures )
            outer_ids.emplace_back(c.Id());
    }

    // Install a primary version of the function globally.  This is used
    // by both broker (for transmitting closures) and script optimization
    // (replacing its AST body with a compiled one).
    primary_func = make_intrusive<ScriptFunc>(ingredients->GetID());
    primary_func->SetOuterIDs(outer_ids);

    // When we build the body, it will get updated with initialization
    // statements.  Update the ingredients to reflect the new body,
    // and no more need for initializers.
    primary_func->AddBody(*ingredients);
    primary_func->SetScope(ingredients->Scope());
    ingredients->ClearInits();

    if ( name.empty() )
        BuildName();
    else
        my_name = std::move(name);

    // Install that in the current scope.
    lambda_id = install_ID(my_name.c_str(), current_module.c_str(), true, false);

    // Update lamb's name
    primary_func->SetName(lambda_id->Name());

    auto v = make_intrusive<FuncVal>(primary_func);
    lambda_id->SetVal(std::move(v));
    lambda_id->SetType(ingr_t);
    lambda_id->SetConst();

    analyze_lambda(this);
}

LambdaExpr::LambdaExpr(LambdaExpr* orig) : Expr(EXPR_LAMBDA) {
    primary_func = orig->primary_func;
    ingredients = orig->ingredients;
    lambda_id = orig->lambda_id;
    my_name = orig->my_name;
    private_captures = orig->private_captures;

    // We need to have our own copies of the outer IDs and captures so
    // we can rename them when inlined.
    for ( auto& i : orig->outer_ids )
        outer_ids.emplace_back(i);

    if ( orig->captures ) {
        captures = std::vector<FuncType::Capture>{};
        for ( auto& c : *orig->captures )
            captures->push_back(c);
    }

    SetType(orig->GetType());
}

bool LambdaExpr::CheckCaptures(StmtPtr when_parent) {
    auto desc = when_parent ? "\"when\" statement" : "lambda";

    if ( ! captures ) {
        if ( ! outer_ids.empty() ) {
            reporter->Error("%s uses outer identifiers without [] captures: %s%s", desc,
                            outer_ids.size() > 1 ? "e.g., " : "", outer_ids[0]->Name());
            return false;
        }

        return true;
    }

    std::unordered_set<IDPtr> outer_is_matched;
    std::unordered_set<IDPtr> capture_is_matched;

    for ( const auto& c : *captures ) {
        auto cid = c.Id();

        if ( ! cid )
            // This happens for undefined/inappropriate
            // identifiers listed in captures.  There's
            // already been an error message.
            continue;

        if ( capture_is_matched.contains(cid) ) {
            auto msg = util::fmt("%s listed multiple times in capture", cid->Name());
            if ( when_parent )
                when_parent->Error(msg);
            else
                ExprError(msg);

            return false;
        }

        for ( const auto& id : outer_ids )
            if ( cid == id ) {
                outer_is_matched.insert(id);
                capture_is_matched.insert(cid);
                break;
            }
    }

    for ( const auto& id : outer_ids )
        if ( ! outer_is_matched.contains(id) ) {
            auto msg = util::fmt("%s is used inside %s but not captured", id->Name(), desc);
            if ( when_parent )
                when_parent->Error(msg);
            else
                ExprError(msg);

            return false;
        }

    for ( const auto& c : *captures ) {
        const auto& cid = c.Id();
        if ( cid && ! capture_is_matched.contains(cid) ) {
            auto msg = util::fmt("%s is captured but not used inside %s", cid->Name(), desc);
            if ( when_parent )
                when_parent->Error(msg);
            else
                ExprError(msg);

            return false;
        }
    }

    return true;
}

void LambdaExpr::BuildName() {
    // Get the body's "string" representation.
    ODesc d;
    primary_func->Describe(&d);

    if ( captures )
        for ( auto& c : *captures ) {
            if ( c.IsDeepCopy() )
                d.AddSP("copy");

            if ( c.Id() )
                // c.Id() will be nil for some errors
                c.Id()->Describe(&d);
        }

    for ( ;; ) {
        hash128_t h;
        KeyedHash::Hash128(d.Bytes(), d.Size(), &h);

        my_name = "lambda_<" + std::to_string(h[0]) + ">";
        auto fullname = make_full_var_name(current_module.data(), my_name.data());
        const auto& id = current_scope()->Find(fullname);

        if ( id )
            // Just try again to make a unique lambda name.
            // If two peer processes need to agree on the same
            // lambda name, this assumes they're loading the same
            // scripts and thus have the same hash collisions.
            d.Add(" ");
        else
            break;
    }
}

ScopePtr LambdaExpr::GetScope() const { return ingredients->Scope(); }

void LambdaExpr::ReplaceBody(StmtPtr new_body) { ingredients->ReplaceBody(std::move(new_body)); }

ValPtr LambdaExpr::Eval(Frame* f) const {
    auto lamb = make_intrusive<ScriptFunc>(ingredients->GetID());

    // Use the primary function as the source of the frame size
    // and function body, rather than the ingredients, since script
    // optimization might have changed the former but not the latter.
    lamb->SetFrameSize(primary_func->FrameSize());
    StmtPtr body = primary_func->GetBodies()[0].stmts;

    if ( run_state::is_parsing ) {
        // We're evaluating this lambda at parse time, which happens
        // for initializations.  If we're doing script optimization
        // then the current version of the body might be left in an
        // inconsistent state (e.g., if it's replaced with ZAM code)
        // causing problems if we execute this lambda subsequently.
        // To avoid that problem, we duplicate the AST so it's
        // distinct, and we inform script optimization so it can track
        // the alias we're introducing.
        auto orig_body = body;
        body = body->Duplicate();
        register_lambda_alias(orig_body, body);
    }

    lamb->AddBody(*ingredients, body);
    lamb->CreateCaptures(f);

    // Set name to corresponding master func.
    // Allows for lookups by the receiver.
    lamb->SetName(my_name.c_str());

    return make_intrusive<FuncVal>(std::move(lamb));
}

void LambdaExpr::ExprDescribe(ODesc* d) const {
    type->Describe(d);

    if ( captures && d->IsReadable() ) {
        d->Add("[");

        for ( auto& c : *captures ) {
            if ( &c != &(*captures)[0] )
                d->AddSP(", ");

            if ( c.IsDeepCopy() )
                d->AddSP("copy");

            d->Add(c.Id()->Name());
        }

        d->Add("]");
    }

    ingredients->Body()->Describe(d);
}

TraversalCode LambdaExpr::Traverse(TraversalCallback* cb) const {
    if ( IsError() )
        // Not well-formed.
        return TC_CONTINUE;

    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    tc = lambda_id->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = ingredients->Body()->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

EventExpr::EventExpr(const char* arg_name, ListExprPtr arg_args)
    : Expr(EXPR_EVENT), name(arg_name), args(std::move(arg_args)) {
    EventHandler* h = event_registry->Lookup(name);

    if ( ! h ) {
        h = new EventHandler(name.c_str());
        event_registry->Register(h, true);
    }

    handler = h;

    if ( args->IsError() ) {
        SetError();
        return;
    }

    const auto& func_type = h->GetType();

    if ( ! func_type ) {
        Error("not an event");
        SetError();
        return;
    }

    if ( record_type_has_errors(func_type->AsFuncType()->Params()->AsRecordType()) )
        SetError();
    else if ( ! func_type->MatchesIndex(args.get()) )
        SetError("argument type mismatch in event invocation");
    else {
        if ( func_type->Yield() ) {
            Error("function invoked as an event");
            SetError();
        }
    }
}

ValPtr EventExpr::Eval(Frame* f) const {
    if ( IsError() )
        return nullptr;

    auto v = eval_list(f, args.get());

    if ( handler ) {
        if ( event_trace_mgr )
            event_trace_mgr->ScriptEventQueued(handler);

        event_mgr.Enqueue(handler, std::move(*v));
    }

    return nullptr;
}

TraversalCode EventExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    auto& f = handler->GetFunc();
    if ( f ) {
        // We don't traverse the function, because that can lead
        // to infinite traversals.  We do, however, see if we can
        // locate the corresponding identifier, and traverse that.

        auto& id = lookup_ID(f->GetName().c_str(), GLOBAL_MODULE_NAME, false, false, false);

        if ( id ) {
            tc = id->Traverse(cb);
            HANDLE_TC_EXPR_PRE(tc);
        }
    }

    tc = args->Traverse(cb);
    HANDLE_TC_EXPR_PRE(tc);

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

void EventExpr::ExprDescribe(ODesc* d) const {
    d->Add(name.c_str());
    if ( d->IsReadable() ) {
        d->Add("(");
        args->Describe(d);
        d->Add(")");
    }
    else
        args->Describe(d);
}

ListExpr::ListExpr() : Expr(EXPR_LIST) { SetType(make_intrusive<TypeList>()); }

ListExpr::ListExpr(ExprPtr e) : Expr(EXPR_LIST) {
    SetType(make_intrusive<TypeList>());
    Append(std::move(e));
}

ListExpr::~ListExpr() {
    for ( const auto& expr : exprs )
        Unref(expr);
}

void ListExpr::Append(ExprPtr e) {
    exprs.push_back(e.release());
    (static_cast<TypeList*>(type.get()))->Append(exprs.back()->GetType());
}

bool ListExpr::IsPure() const {
    for ( const auto& expr : exprs )
        if ( ! expr->IsPure() )
            return false;

    return true;
}

bool ListExpr::HasConstantOps() const {
    loop_over_list(exprs, i) if ( exprs[i]->Tag() != EXPR_CONST ) return false;

    return true;
}

ValPtr ListExpr::Eval(Frame* f) const {
    std::vector<ValPtr> evs;

    for ( const auto& expr : exprs ) {
        auto ev = expr->Eval(f);

        if ( ! ev ) {
            RuntimeError("uninitialized list value");
            return nullptr;
        }

        evs.push_back(std::move(ev));
    }

    return make_intrusive<ListVal>(cast_intrusive<TypeList>(type), std::move(evs));
}

TypePtr ListExpr::InitType() const {
    if ( exprs.empty() ) {
        Error("empty list in untyped initialization");
        return nullptr;
    }

    if ( exprs[0]->IsRecordElement(nullptr) ) {
        type_decl_list* types = new type_decl_list(exprs.length());
        for ( const auto& expr : exprs ) {
            TypeDecl* td = new TypeDecl(nullptr, nullptr);
            if ( ! expr->IsRecordElement(td) ) {
                expr->Error("record element expected");
                delete td;
                delete types;
                return nullptr;
            }

            types->push_back(td);
        }

        return make_intrusive<RecordType>(types);
    }

    else {
        auto tl = make_intrusive<TypeList>();

        for ( const auto& e : exprs ) {
            const auto& ti = e->GetType();

            // Collapse any embedded sets or lists.
            if ( ti->IsSet() || ti->Tag() == TYPE_LIST ) {
                TypeList* til = ti->IsSet() ? ti->AsTableType()->GetIndices().get() : ti->AsTypeList();

                if ( ! til->IsPure() || ! til->AllMatch(til->GetPureType(), true) )
                    tl->Append({NewRef{}, til});
                else
                    tl->Append(til->GetPureType());
            }
            else
                tl->Append(ti);
        }

        return tl;
    }
}

void ListExpr::ExprDescribe(ODesc* d) const {
    d->AddCount(exprs.length());

    loop_over_list(exprs, i) {
        if ( d->IsReadable() && i > 0 )
            d->Add(", ");

        exprs[i]->Describe(d);
    }
}

ExprPtr ListExpr::MakeLvalue() {
    for ( const auto& expr : exprs )
        if ( expr->Tag() != EXPR_NAME )
            ExprError("can only assign to list of identifiers");

    return with_location_of(make_intrusive<RefExpr>(ThisPtr()), this);
}

void ListExpr::Assign(Frame* f, ValPtr v) {
    ListVal* lv = v->AsListVal();

    if ( exprs.length() != lv->Length() )
        RuntimeError("mismatch in list lengths");

    loop_over_list(exprs, i) exprs[i]->Assign(f, lv->Idx(i));
}

TraversalCode ListExpr::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreExpr(this);
    HANDLE_TC_EXPR_PRE(tc);

    for ( const auto& expr : exprs ) {
        tc = expr->Traverse(cb);
        HANDLE_TC_EXPR_PRE(tc);
    }

    tc = cb->PostExpr(this);
    HANDLE_TC_EXPR_POST(tc);
}

RecordAssignExpr::RecordAssignExpr(const ExprPtr& record, const ExprPtr& init_list, bool is_init) {
    const ExprPList& inits = init_list->AsListExpr()->Exprs();

    RecordType* lhs = record->GetType()->AsRecordType();

    // The inits have two forms:
    // 1) other records -- use all matching field names+types
    // 2) a string indicating the field name, then (as the next element)
    //    the value to use for that field.

    for ( const auto& init : inits ) {
        if ( init->GetType()->Tag() == TYPE_RECORD ) {
            RecordType* t = init->GetType()->AsRecordType();

            for ( int j = 0; j < t->NumFields(); ++j ) {
                const char* field_name = t->FieldName(j);
                int field = lhs->FieldOffset(field_name);

                if ( field >= 0 && same_type(lhs->GetFieldType(field), t->GetFieldType(j)) ) {
                    auto fe_lhs = with_location_of(make_intrusive<FieldExpr>(record, field_name), init_list);
                    auto fe_rhs = with_location_of(make_intrusive<FieldExpr>(IntrusivePtr{NewRef{}, init}, field_name),
                                                   init_list);
                    Append(get_assign_expr(std::move(fe_lhs), std::move(fe_rhs), is_init));
                }
            }
        }

        else if ( init->Tag() == EXPR_FIELD_ASSIGN ) {
            FieldAssignExpr* rf = static_cast<FieldAssignExpr*>(init);
            rf->Ref();

            const char* field_name = ""; // rf->FieldName();
            if ( lhs->HasField(field_name) ) {
                auto fe_lhs = with_location_of(make_intrusive<FieldExpr>(record, field_name), init_list);
                ExprPtr fe_rhs = {NewRef{}, rf->Op()};
                Append(get_assign_expr(std::move(fe_lhs), std::move(fe_rhs), is_init));
            }
            else {
                std::string s = "No such field '";
                s += field_name;
                s += '\'';
                init_list->SetError(s.c_str());
            }
        }

        else {
            init_list->SetError("bad record initializer");
            return;
        }
    }
}

CanConvertExpr::CanConvertExpr(ExprPtr arg_op, TypePtr t)
    : UnaryExpr(EXPR_CAN_CONVERT, std::move(arg_op)), conversion_type(std::move(t)) {
    auto stype = Op()->GetType();
    SetType(base_type(TYPE_BOOL));
}

ValPtr CanConvertExpr::Fold(Val* v) const {
    if ( attempt_to_cast_value_to_type(v, conversion_type.get()) )
        return val_mgr->True();
    else
        return val_mgr->False();
}

void CanConvertExpr::ExprDescribe(ODesc* d) const {
    Op()->Describe(d);
    d->Add(" ?as ");
    conversion_type->Describe(d);
}

CastExpr::CastExpr(ExprPtr arg_op, TypePtr t) : UnaryExpr(EXPR_CAST, std::move(arg_op)) {
    auto stype = Op()->GetType();

    SetType(std::move(t));

    if ( ! IsError() && ! can_cast_type_to_type(stype.get(), GetType().get()) )
        ExprError("cast not supported");
}

ValPtr CastExpr::Fold(Val* v) const {
    std::string error;
    auto res = cast_value({NewRef{}, v}, GetType(), error);

    if ( ! res )
        RuntimeError(error.c_str());

    return res;
}

ValPtr cast_value(ValPtr v, const TypePtr& t, std::string& error) {
    auto nv = cast_value_to_type(v.get(), t.get());

    if ( nv )
        return nv;

    ODesc d;

    d.Add("invalid cast of value with type '");
    v->GetType()->Describe(&d);
    d.Add("' to type '");
    t->Describe(&d);
    d.Add("'");

    if ( same_type(v->GetType(), Broker::detail::DataVal::ScriptDataType()) && ! v->AsRecordVal()->HasField(0) )
        d.Add(" (nil $data field)");

    error = d.Description();
    return nullptr;
}

void CastExpr::ExprDescribe(ODesc* d) const {
    Op()->Describe(d);
    d->Add(" as ");
    GetType()->Describe(d);
}

IsExpr::IsExpr(ExprPtr arg_op, TypePtr arg_t) : UnaryExpr(EXPR_IS, std::move(arg_op)), t(std::move(arg_t)) {
    SetType(base_type(TYPE_BOOL));
}

ValPtr IsExpr::Fold(Val* v) const {
    if ( IsError() )
        return nullptr;

    return val_mgr->Bool(can_cast_any_to_type(v, t.get()));
}

void IsExpr::ExprDescribe(ODesc* d) const {
    Op()->Describe(d);
    d->Add(" is ");
    t->Describe(d);
}

ExprPtr get_assign_expr(ExprPtr op1, ExprPtr op2, bool is_init) {
    ExprPtr e;

    if ( op1->GetType()->Tag() == TYPE_RECORD && op2->GetType()->Tag() == TYPE_LIST )
        e = make_intrusive<RecordAssignExpr>(op1, std::move(op2), is_init);

    else if ( op1->Tag() == EXPR_INDEX && op1->AsIndexExpr()->IsSlice() )
        e = make_intrusive<IndexSliceAssignExpr>(op1, std::move(op2), is_init);

    else
        e = make_intrusive<AssignExpr>(op1, std::move(op2), is_init);

    e->SetLocationInfo(op1->GetLocationInfo());
    return e;
}

ExprPtr check_and_promote_expr(ExprPtr e, TypePtr t) {
    const auto& et = e->GetType();
    TypeTag e_tag = et->Tag();
    TypeTag t_tag = t->Tag();

    if ( t_tag == TYPE_ANY ) {
        if ( e_tag != TYPE_ANY )
            return with_location_of(make_intrusive<CoerceToAnyExpr>(e), e);

        return e;
    }

    if ( e_tag == TYPE_ANY )
        return with_location_of(make_intrusive<CoerceFromAnyExpr>(e, t), e);

    if ( EitherArithmetic(t_tag, e_tag) ) {
        if ( e_tag == t_tag )
            return e;

        if ( ! BothArithmetic(t_tag, e_tag) ) {
            t->Error("arithmetic mixed with non-arithmetic", e.get());
            return nullptr;
        }

        TypeTag mt = max_type(t_tag, e_tag);
        if ( mt != t_tag ) {
            t->Error("over-promotion of arithmetic value", e.get());
            return nullptr;
        }

        return with_location_of(make_intrusive<ArithCoerceExpr>(e, t_tag), e);
    }

    if ( t->Tag() == TYPE_RECORD && et->Tag() == TYPE_RECORD ) {
        RecordType* t_r = t->AsRecordType();
        RecordType* et_r = et->AsRecordType();

        if ( same_type(t, et) )
            return e;

        if ( record_promotion_compatible(t_r, et_r) )
            return with_location_of(make_intrusive<RecordCoerceExpr>(e, IntrusivePtr{NewRef{}, t_r}), e);

        t->Error("incompatible record types", e.get());
        return nullptr;
    }

    if ( ! same_type(t, et, false, false) ) {
        if ( t->Tag() == TYPE_TABLE && et->Tag() == TYPE_TABLE && et->AsTableType()->IsUnspecifiedTable() ) {
            if ( e->Tag() == EXPR_TABLE_CONSTRUCTOR ) {
                auto& attrs = cast_intrusive<TableConstructorExpr>(e)->GetAttrs();
                zeek::detail::AttrPtr def = Attr::nil;

                // Check for &default or &default_insert expressions
                // and use it for type checking against t.
                if ( attrs ) {
                    def = attrs->Find(ATTR_DEFAULT);
                    if ( ! def )
                        def = attrs->Find(ATTR_DEFAULT_INSERT);
                }

                if ( def ) {
                    std::string err_msg;
                    if ( ! check_default_attr(def.get(), t, false, false, err_msg) ) {
                        if ( ! err_msg.empty() )
                            t->Error(err_msg.c_str(), e.get());
                        return nullptr;
                    }
                }
            }

            return with_location_of(make_intrusive<TableCoerceExpr>(e, IntrusivePtr{NewRef{}, t->AsTableType()}, false),
                                    e);
        }

        if ( t->Tag() == TYPE_VECTOR && et->Tag() == TYPE_VECTOR && et->AsVectorType()->IsUnspecifiedVector() )
            return with_location_of(make_intrusive<VectorCoerceExpr>(e, IntrusivePtr{NewRef{}, t->AsVectorType()}), e);

        if ( t->Tag() != TYPE_ERROR && et->Tag() != TYPE_ERROR )
            t->Error("type clash", e.get());

        return nullptr;
    }

    return e;
}

bool check_and_promote_exprs(ListExpr* const elements, const TypeListPtr& types) {
    ExprPList& el = elements->Exprs();
    const auto& tl = types->GetTypes();

    if ( tl.size() == 1 && tl[0]->Tag() == TYPE_ANY )
        return true;

    if ( el.length() != static_cast<int>(tl.size()) ) {
        types->Error("indexing mismatch", elements);
        return false;
    }

    loop_over_list(el, i) {
        ExprPtr e = {NewRef{}, el[i]};
        auto promoted_e = check_and_promote_expr(e, tl[i]);

        if ( ! promoted_e ) {
            e->Error("type mismatch", tl[i].get());
            return false;
        }

        if ( promoted_e != e )
            Unref(el.replace(i, promoted_e.release()));
    }

    return true;
}

bool check_and_promote_args(ListExpr* const args, const RecordType* types) {
    ExprPList& el = args->Exprs();
    int ntypes = types->NumFields();

    // give variadic BIFs automatic pass
    if ( ntypes == 1 && types->FieldDecl(0)->type->Tag() == TYPE_ANY )
        return true;

    if ( el.length() < ntypes ) {
        std::vector<ExprPtr> def_elements;

        // Start from rightmost parameter, work backward to fill in missing
        // arguments using &default expressions.
        for ( int i = ntypes - 1; i >= el.length(); --i ) {
            auto td = types->FieldDecl(i);
            const auto& def_attr = td->attrs ? td->attrs->Find(ATTR_DEFAULT).get() : nullptr;

            if ( ! def_attr ) {
                types->Error("parameter mismatch", args);
                return false;
            }

            // Don't use the default expression directly, as
            // doing so will wind up sharing its code across
            // different invocations that use the default
            // argument.  That works okay for the interpreter,
            // but if we transform the code we want that done
            // separately for each instance, rather than
            // one instance inheriting the transformed version
            // from another.
            const auto& e = def_attr->GetExpr();
            def_elements.emplace_back(e->Duplicate());
        }

        auto ne = def_elements.size();
        while ( ne )
            el.push_back(def_elements[--ne].release());
    }

    auto tl = make_intrusive<TypeList>();

    for ( int i = 0; i < types->NumFields(); ++i )
        tl->Append(types->GetFieldType(i));

    int rval = check_and_promote_exprs(args, tl);

    return rval;
}

bool check_and_promote_exprs_to_type(ListExpr* const elements, TypePtr t) {
    ExprPList& el = elements->Exprs();

    if ( t->Tag() == TYPE_ANY )
        return true;

    loop_over_list(el, i) {
        ExprPtr e = {NewRef{}, el[i]};
        auto promoted_e = check_and_promote_expr(e, t);

        if ( ! promoted_e ) {
            e->Error("type mismatch", t.get());
            return false;
        }

        if ( promoted_e != e )
            Unref(el.replace(i, promoted_e.release()));
    }

    return true;
}

std::optional<std::vector<ValPtr>> eval_list(Frame* f, const ListExpr* l) {
    const ExprPList& e = l->Exprs();
    auto rval = std::make_optional<std::vector<ValPtr>>();
    rval->reserve(e.length());

    for ( const auto& expr : e ) {
        auto ev = expr->Eval(f);

        if ( ! ev )
            return {};

        rval->emplace_back(std::move(ev));
    }

    return rval;
}

ValPtr eval_in_isolation(const Expr* e) {
    try {
        return e->Eval(nullptr);
    } catch ( IsolationException& ) {
        return nullptr;
    }
}

bool expr_greater(const Expr* e1, const Expr* e2) { return e1->Tag() > e2->Tag(); }

} // namespace zeek::detail
