// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/Type.h"

#include <algorithm>
#include <cinttypes>
#include <map>
#include <string>
#include <unordered_set>

#include "zeek/Attr.h"
#include "zeek/CompHash.h"
#include "zeek/Desc.h"
#include "zeek/Expr.h"
#include "zeek/Reporter.h"
#include "zeek/RunState.h"
#include "zeek/Scope.h"
#include "zeek/Traverse.h"
#include "zeek/Val.h"
#include "zeek/Var.h"
#include "zeek/ZVal.h"
#include "zeek/module_util.h"
#include "zeek/zeekygen/IdentifierInfo.h"
#include "zeek/zeekygen/Manager.h"
#include "zeek/zeekygen/ScriptInfo.h"
#include "zeek/zeekygen/utils.h"

using namespace std;

namespace zeek {

Type::TypeAliasMap Type::type_aliases;

// Note: This function must be thread-safe.
const char* type_name(TypeTag t) {
    static constexpr const char* type_names[static_cast<int>(NUM_TYPES)] = {
        "void",     // 0
        "bool",     // 1
        "int",      // 2
        "count",    // 3
        "double",   // 4
        "time",     // 5
        "interval", // 6
        "string",   // 7
        "pattern",  // 8
        "enum",     // 9
        "port",     // 10
        "addr",     // 11
        "subnet",   // 12
        "any",      // 13
        "table",    // 14
        "record",   // 15
        "types",    // 16
        "func",     // 17
        "file",     // 18
        "vector",   // 19
        "opaque",   // 20
        "type",     // 21
        "error",    // 22
    };

    if ( static_cast<int>(t) >= NUM_TYPES )
        return "type_name(): not a type tag";

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

Type::Type(TypeTag t, bool arg_base_type)
    : tag(t),
      internal_tag(to_internal_type_tag(tag)),
      is_network_order(zeek::is_network_order(t)),
      base_type(arg_base_type) {}

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define CHECK_TYPE_TAG(tag_type, func_name) CHECK_TAG(tag, tag_type, func_name, type_name)

const TypeList* Type::AsTypeList() const {
    CHECK_TYPE_TAG(TYPE_LIST, "Type::AsTypeList");
    return static_cast<const TypeList*>(this);
}

TypeList* Type::AsTypeList() {
    CHECK_TYPE_TAG(TYPE_LIST, "Type::AsTypeList");
    return static_cast<TypeList*>(this);
}

const TableType* Type::AsTableType() const {
    CHECK_TYPE_TAG(TYPE_TABLE, "Type::AsTableType");
    return static_cast<const TableType*>(this);
}

TableType* Type::AsTableType() {
    CHECK_TYPE_TAG(TYPE_TABLE, "Type::AsTableType");
    return static_cast<TableType*>(this);
}

#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
const SetType* Type::AsSetType() const {
    if ( ! IsSet() )
        BadTag("Type::AsSetType", type_name(tag));
    return static_cast<const SetType*>(this);
}

SetType* Type::AsSetType() {
    if ( ! IsSet() )
        BadTag("Type::AsSetType", type_name(tag));
    return static_cast<SetType*>(this);
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

const RecordType* Type::AsRecordType() const {
    CHECK_TYPE_TAG(TYPE_RECORD, "Type::AsRecordType");
    return static_cast<const RecordType*>(this);
}

RecordType* Type::AsRecordType() {
    CHECK_TYPE_TAG(TYPE_RECORD, "Type::AsRecordType");
    return static_cast<RecordType*>(this);
}

const FuncType* Type::AsFuncType() const {
    CHECK_TYPE_TAG(TYPE_FUNC, "Type::AsFuncType");
    return static_cast<const FuncType*>(this);
}

FuncType* Type::AsFuncType() {
    CHECK_TYPE_TAG(TYPE_FUNC, "Type::AsFuncType");
    return static_cast<FuncType*>(this);
}

const FileType* Type::AsFileType() const {
    CHECK_TYPE_TAG(TYPE_FILE, "Type::AsFileType");
    return static_cast<const FileType*>(this);
}

FileType* Type::AsFileType() {
    CHECK_TYPE_TAG(TYPE_FILE, "Type::AsFileType");
    return static_cast<FileType*>(this);
}

const EnumType* Type::AsEnumType() const {
    CHECK_TYPE_TAG(TYPE_ENUM, "Type::AsEnumType");
    return static_cast<const EnumType*>(this);
}

EnumType* Type::AsEnumType() {
    CHECK_TYPE_TAG(TYPE_ENUM, "Type::AsEnumType");
    return static_cast<EnumType*>(this);
}

const VectorType* Type::AsVectorType() const {
    CHECK_TYPE_TAG(TYPE_VECTOR, "Type::AsVectorType");
    return static_cast<const VectorType*>(this);
}

VectorType* Type::AsVectorType() {
    CHECK_TYPE_TAG(TYPE_VECTOR, "Type::AsVectorType");
    return static_cast<VectorType*>(this);
}

const OpaqueType* Type::AsOpaqueType() const {
    CHECK_TYPE_TAG(TYPE_OPAQUE, "Type::AsOpaqueType");
    return static_cast<const OpaqueType*>(this);
}

OpaqueType* Type::AsOpaqueType() {
    CHECK_TYPE_TAG(TYPE_OPAQUE, "Type::AsOpaqueType");
    return static_cast<OpaqueType*>(this);
}

const TypeType* Type::AsTypeType() const {
    CHECK_TYPE_TAG(TYPE_TYPE, "Type::AsTypeType");
    return static_cast<const TypeType*>(this);
}

TypeType* Type::AsTypeType() {
    CHECK_TYPE_TAG(TYPE_TYPE, "Type::AsTypeType");
    return static_cast<TypeType*>(this);
}

TypePtr Type::ShallowClone() {
    switch ( tag ) {
        case TYPE_VOID:
        case TYPE_BOOL:
        case TYPE_INT:
        case TYPE_COUNT:
        case TYPE_DOUBLE:
        case TYPE_TIME:
        case TYPE_INTERVAL:
        case TYPE_STRING:
        case TYPE_PATTERN:
        case TYPE_PORT:
        case TYPE_ADDR:
        case TYPE_SUBNET:
        case TYPE_ANY: return make_intrusive<Type>(tag, base_type);

        default: reporter->InternalError("cloning illegal base Type");
    }
    return nullptr;
}

int Type::MatchesIndex(detail::ListExpr* const index) const {
    if ( Tag() == TYPE_STRING ) {
        if ( index->Exprs().length() != 1 && index->Exprs().length() != 2 )
            return DOES_NOT_MATCH_INDEX;

        if ( check_and_promote_exprs_to_type(index, zeek::base_type(TYPE_INT)) )
            return MATCHES_INDEX_SCALAR;
    }

    return DOES_NOT_MATCH_INDEX;
}

const TypePtr& Type::Yield() const { return Type::nil; }

void Type::Describe(ODesc* d) const {
    if ( ! d->IsBinary() && ! name.empty() )
        d->Add(name);
    else
        DoDescribe(d);
}

void Type::DoDescribe(ODesc* d) const {
    if ( d->IsBinary() )
        d->Add(static_cast<int>(Tag()));
    else {
        TypeTag t = Tag();
        if ( IsSet() )
            d->Add("set");
        else
            d->Add(type_name(t));
    }
}

void Type::DescribeReST(ODesc* d, bool roles_only) const { d->Add(util::fmt(":zeek:type:`%s`", type_name(Tag()))); }

void Type::SetError() { tag = TYPE_ERROR; }

detail::TraversalCode Type::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

void TypeList::CheckPure() {
    if ( pure_type )
        return;

    if ( ! types.empty() && AllMatch(types[0], false) )
        pure_type = types[0];
}

bool TypeList::AllMatch(const Type* t, bool is_init) const {
    for ( const auto& type : types )
        if ( ! same_type(type, t, is_init) )
            return false;
    return true;
}

void TypeList::Append(TypePtr t) {
    if ( pure_type && ! same_type(t, pure_type) )
        reporter->InternalError("pure type-list violation t=%s (%p) pure_type=%s (%p)", obj_desc_short(t).c_str(),
                                static_cast<void*>(t.get()), obj_desc_short(pure_type).c_str(),
                                static_cast<void*>(pure_type.get()));

    types.emplace_back(std::move(t));
}

void TypeList::AppendEvenIfNotPure(TypePtr t) {
    if ( pure_type && ! same_type(t, pure_type) )
        pure_type = nullptr;

    types.emplace_back(std::move(t));
}

void TypeList::DoDescribe(ODesc* d) const {
    if ( d->IsReadable() )
        d->AddSP("list of");
    else {
        d->Add(static_cast<int>(Tag()));
        d->Add(IsPure());
        if ( IsPure() )
            pure_type->Describe(d);
        d->Add(static_cast<uint64_t>(types.size()));
    }

    if ( IsPure() )
        pure_type->Describe(d);
    else {
        for ( size_t i = 0; i < types.size(); ++i ) {
            if ( i > 0 && ! d->IsBinary() )
                d->Add(",");

            types[i]->Describe(d);
        }
    }
}

detail::TraversalCode TypeList::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    for ( const auto& type : types ) {
        tc = type->Traverse(cb);
        HANDLE_TC_TYPE_PRE(tc);
    }

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

int IndexType::MatchesIndex(detail::ListExpr* const index) const {
    // If we have a type indexed by subnets, addresses are ok.
    const auto& types = indices->GetTypes();
    const ExprPList& exprs = index->Exprs();

    if ( types.size() == 1 && types[0]->Tag() == TYPE_SUBNET && exprs.length() == 1 &&
         exprs[0]->GetType()->Tag() == TYPE_ADDR )
        return MATCHES_INDEX_SCALAR;

    return check_and_promote_exprs(index, GetIndices()) ? MATCHES_INDEX_SCALAR : DOES_NOT_MATCH_INDEX;
}

void IndexType::DoDescribe(ODesc* d) const {
    Type::DoDescribe(d);

    const auto& its = GetIndexTypes();

    // Deal with unspecified table/set.
    if ( its.empty() ) {
        if ( ! d->IsBinary() )
            d->Add("()");
        return;
    }

    if ( ! d->IsBinary() )
        d->Add("[");

    for ( auto i = 0u; i < its.size(); ++i ) {
        if ( ! d->IsBinary() && i > 0 )
            d->Add(",");
        its[i]->Describe(d);
    }
    if ( ! d->IsBinary() )
        d->Add("]");

    if ( yield_type ) {
        if ( ! d->IsBinary() )
            d->Add(" of ");
        yield_type->Describe(d);
    }
}

void IndexType::DescribeReST(ODesc* d, bool roles_only) const {
    d->Add(":zeek:type:`");

    if ( IsSet() )
        d->Add("set");
    else
        d->Add(type_name(Tag()));

    d->Add("` ");
    d->Add("[");

    const auto& its = GetIndexTypes();

    for ( auto i = 0u; i < its.size(); ++i ) {
        if ( i > 0 )
            d->Add(", ");

        const auto& t = its[i];

        if ( ! t->GetName().empty() ) {
            d->Add(":zeek:type:`");
            d->Add(t->GetName());
            d->Add("`");
        }
        else
            t->DescribeReST(d, roles_only);
    }

    d->Add("]");

    if ( yield_type ) {
        d->Add(" of ");

        if ( ! yield_type->GetName().empty() ) {
            d->Add(":zeek:type:`");
            d->Add(yield_type->GetName());
            d->Add("`");
        }
        else
            yield_type->DescribeReST(d, roles_only);
    }
}

detail::TraversalCode IndexType::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    for ( const auto& ind : GetIndexTypes() ) {
        tc = ind->Traverse(cb);
        HANDLE_TC_TYPE_PRE(tc);
    }

    if ( yield_type ) {
        tc = yield_type->Traverse(cb);
        HANDLE_TC_TYPE_PRE(tc);
    }

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

static bool is_supported_index_type(const TypePtr& t, const char** tname, std::unordered_set<TypePtr>& seen) {
    if ( t->InternalType() != TYPE_INTERNAL_OTHER )
        return true;

    // Handle recursive calls as good: If they turn out not
    // to be, that should've been discovered further down.
    if ( seen.contains(t) )
        return true;

    seen.insert(t);

    auto tag = t->Tag();

    switch ( tag ) {
        // Allow functions, since they can be compared for Func* pointer equality.
        case TYPE_FUNC:
        case TYPE_PATTERN: return true;

        case TYPE_RECORD: {
            auto rt = t->AsRecordType();

            for ( auto i = 0; i < rt->NumFields(); ++i )
                if ( ! is_supported_index_type(rt->GetFieldType(i), tname, seen) )
                    return false;

            return true;
        }

        case TYPE_LIST: {
            for ( const auto& type : t->AsTypeList()->GetTypes() )
                if ( ! is_supported_index_type(type, tname, seen) )
                    return false;

            return true;
        }

        case TYPE_TABLE: {
            auto tt = t->AsTableType();

            if ( ! is_supported_index_type(tt->GetIndices(), tname, seen) )
                return false;

            const auto& yt = tt->Yield();

            if ( ! yt )
                return true;

            return is_supported_index_type(yt, tname, seen);
        }

        case TYPE_VECTOR: return is_supported_index_type(t->AsVectorType()->Yield(), tname, seen);

        case TYPE_OPAQUE:
            if ( t->AsOpaqueType()->SupportsHashing() )
                return true;
            *tname = type_name(tag);
            return false;

        default: *tname = type_name(tag); return false;
    }
}

TableType::TableType(TypeListPtr ind, TypePtr yield) : IndexType(TYPE_TABLE, std::move(ind), std::move(yield)) {
    if ( ! indices )
        return;

    const auto& tl = indices->GetTypes();
    const char* unsupported_type_name = nullptr;

    for ( const auto& tli : tl ) {
        InternalTypeTag t = tli->InternalType();

        if ( t == TYPE_INTERNAL_ERROR )
            break;

        std::unordered_set<TypePtr> seen;
        if ( ! is_supported_index_type(tli, &unsupported_type_name, seen) ) {
            auto msg = util::fmt("index type containing '%s' is not supported", unsupported_type_name);
            Error(msg, tli.get());
            SetError();
            break;
        }
    }

    if ( Tag() != TYPE_ERROR )
        RegenerateHash();
}

TableType::~TableType() = default;

bool TableType::CheckExpireFuncCompatibility(const detail::AttrPtr& attr) {
    if ( reported_error )
        return false;

    bool success = DoExpireCheck(attr);
    if ( ! success )
        reported_error = true;

    return success;
}

TypePtr TableType::ShallowClone() { return make_intrusive<TableType>(indices, yield_type); }

void TableType::RegenerateHash() { table_hash = std::make_unique<detail::CompositeHash>(GetIndices()); }

bool TableType::IsUnspecifiedTable() const {
    // Unspecified types have an empty list of indices.
    return indices->GetTypes().empty();
}

bool TableType::DoExpireCheck(const detail::AttrPtr& attr) {
    assert(attr->Tag() == detail::ATTR_EXPIRE_FUNC);

    const auto& expire_func = attr->GetExpr();

    if ( expire_func->GetType()->Tag() != TYPE_FUNC ) {
        attr->Error("&expire_func attribute is not a function");
        return false;
    }

    const FuncType* e_ft = expire_func->GetType()->AsFuncType();

    if ( e_ft->Flavor() != FUNC_FLAVOR_FUNCTION ) {
        attr->Error("&expire_func attribute is not a function");
        return false;
    }

    if ( e_ft->Yield()->Tag() != TYPE_INTERVAL ) {
        attr->Error("&expire_func must yield a value of type interval");
        return false;
    }

    if ( IsUnspecifiedTable() )
        return true;

    const auto& func_index_types = e_ft->ParamList()->GetTypes();
    // Keep backwards compatibility with idx: any idiom.
    if ( func_index_types.size() == 2 ) {
        if ( func_index_types[1]->Tag() == TYPE_ANY )
            return true;
    }

    const auto& table_index_types = GetIndexTypes();

    std::vector<TypePtr> expected_args;
    expected_args.reserve(1 + table_index_types.size());
    expected_args.emplace_back(NewRef{}, this);

    for ( const auto& t : table_index_types )
        expected_args.emplace_back(t);

    if ( ! e_ft->CheckArgs(expected_args) ) {
        attr->Error("&expire_func argument type clash");
        return false;
    }

    return true;
}

SetType::SetType(TypeListPtr ind, detail::ListExprPtr arg_elements)
    : TableType(std::move(ind), nullptr), elements(std::move(arg_elements)) {
    if ( elements ) {
        if ( indices ) { // We already have a type.
            if ( ! check_and_promote_exprs(elements.get(), indices) )
                SetError();
        }
        else {
            TypeList* tl_type = elements->GetType()->AsTypeList();
            const auto& tl = tl_type->GetTypes();

            if ( tl.empty() ) {
                Error("no type given for set");
                SetError();
            }

            else if ( tl.size() == 1 ) {
                TypePtr ft{NewRef{}, flatten_type(tl[0].get())};
                indices = make_intrusive<TypeList>(ft);
                indices->Append(std::move(ft));
            }

            else {
                auto t = merge_types(tl[0], tl[1]);

                for ( size_t i = 2; t && i < tl.size(); ++i )
                    t = merge_types(t, tl[i]);

                if ( ! t ) {
                    Error("bad set type");
                    return;
                }

                indices = make_intrusive<TypeList>(t);
                indices->Append(std::move(t));
            }
        }
    }
}

TypePtr SetType::ShallowClone() { return make_intrusive<SetType>(indices, elements); }

SetType::~SetType() = default;

FuncType::Capture::Capture(detail::IDPtr _id, bool _deep_copy) : id(std::move(_id)), deep_copy(_deep_copy) {
    is_managed = id ? ZVal::IsManagedType(id->GetType()) : false;
    if ( ! is_managed )
        // For non-managed types, deep copying isn't applicable.
        deep_copy = false;
}

FuncType::FuncType(RecordTypePtr arg_args, TypePtr arg_yield, FunctionFlavor arg_flavor)
    : Type(TYPE_FUNC), args(std::move(arg_args)), arg_types(make_intrusive<TypeList>()), yield(std::move(arg_yield)) {
    flavor = arg_flavor;

    bool has_default_arg = false;
    std::map<int, int> offsets;

    for ( int i = 0; i < args->NumFields(); ++i ) {
        const TypeDecl* td = args->FieldDecl(i);

        if ( td->attrs && td->attrs->Find(detail::ATTR_DEFAULT) )
            has_default_arg = true;

        else if ( has_default_arg ) {
            const char* err_str = util::
                fmt("required parameter '%s' must precede "
                    "default parameters",
                    td->id);
            args->Error(err_str);
        }

        arg_types->Append(args->GetFieldType(i));
        offsets[i] = i;
    }

    prototypes.emplace_back(Prototype{false, "", args, std::move(offsets)});
}

TypePtr FuncType::ShallowClone() {
    auto f = make_intrusive<FuncType>();
    f->args = args;
    f->arg_types = arg_types;
    f->yield = yield;
    f->flavor = flavor;
    f->prototypes = prototypes;
    f->captures = captures;
    return f;
}

string FuncType::FlavorString() const {
    switch ( flavor ) {
        case FUNC_FLAVOR_FUNCTION: return "function";

        case FUNC_FLAVOR_EVENT: return "event";

        case FUNC_FLAVOR_HOOK: return "hook";

        default: reporter->InternalError("Invalid function flavor"); return "invalid_func_flavor";
    }
}

int FuncType::MatchesIndex(detail::ListExpr* const index) const {
    return check_and_promote_args(index, args.get()) ? MATCHES_INDEX_SCALAR : DOES_NOT_MATCH_INDEX;
}

bool FuncType::CheckArgs(const TypePList* args, bool is_init, bool do_warn) const {
    std::vector<TypePtr> as;
    as.reserve(args->length());

    for ( auto a : *args )
        as.emplace_back(NewRef{}, a);

    return CheckArgs(as, is_init, do_warn);
}

bool FuncType::CheckArgs(const std::vector<TypePtr>& args, bool is_init, bool do_warn) const {
    if ( reported_error )
        return false;

    const auto& my_args = arg_types->GetTypes();

    if ( my_args.size() != args.size() ) {
        if ( do_warn ) {
            Warn(util::fmt("Wrong number of arguments for function. Expected %zu, got %zu.", args.size(),
                           my_args.size()));
            const_cast<FuncType*>(this)->reported_error = true;
        }

        return false;
    }

    bool success = true;

    for ( size_t i = 0; i < my_args.size(); ++i )
        if ( ! same_type(args[i], my_args[i], is_init) ) {
            if ( do_warn )
                Warn(util::fmt("Type mismatch in function argument #%zu. Expected %s, got %s.", i,
                               type_name(args[i]->Tag()), type_name(my_args[i]->Tag())));
            success = false;
        }

    const_cast<FuncType*>(this)->reported_error = ! success;

    return success;
}

void FuncType::SetCaptures(std::optional<CaptureList> _captures) { captures = std::move(_captures); }

void FuncType::DoDescribe(ODesc* d) const {
    if ( d->IsReadable() ) {
        d->Add(FlavorString());
        d->Add("(");
        args->DescribeFields(d, true);
        d->Add(")");

        if ( yield ) {
            d->AddSP(" :");
            yield->Describe(d);
        }
    }
    else {
        d->Add(static_cast<int>(Tag()));
        d->Add(flavor);
        d->Add(yield != nullptr);
        args->DescribeFields(d, true);
        if ( yield )
            yield->Describe(d);
    }
}

void FuncType::DescribeReST(ODesc* d, bool roles_only) const {
    d->Add(":zeek:type:`");
    d->Add(FlavorString());
    d->Add("`");
    d->Add(" (");
    args->DescribeFieldsReST(d, true);
    d->Add(")");

    if ( yield ) {
        d->AddSP(" :");

        if ( ! yield->GetName().empty() ) {
            d->Add(":zeek:type:`");
            d->Add(yield->GetName());
            d->Add("`");
        }
        else
            yield->DescribeReST(d, roles_only);
    }
}

void FuncType::AddPrototype(Prototype p) { prototypes.emplace_back(std::move(p)); }

std::optional<FuncType::Prototype> FuncType::FindPrototype(const RecordType& args) const {
    for ( const auto& p : prototypes ) {
        if ( args.NumFields() != p.args->NumFields() )
            continue;

        if ( args.NumFields() == 0 ) {
            if ( p.args->NumFields() == 0 )
                return p;

            continue;
        }

        bool matched = true;

        for ( auto i = 0; i < args.NumFields(); ++i ) {
            const auto& ptype = p.args->GetFieldType(i);
            const auto& desired_type = args.GetFieldType(i);

            if ( ! same_type(ptype, desired_type) || ! util::streq(args.FieldName(i), p.args->FieldName(i)) ) {
                matched = false;
                break;
            }
        }

        if ( matched )
            return p;
    }

    return {};
}

detail::TraversalCode FuncType::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

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

    if ( yield ) {
        tc = yield->Traverse(cb);
        HANDLE_TC_TYPE_PRE(tc);
    }

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

detail::TraversalCode TypeType::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    tc = type->Traverse(cb);
    HANDLE_TC_TYPE_PRE(tc);

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

TypeDecl::TypeDecl(const char* i, TypePtr t, detail::AttributesPtr arg_attrs)
    : type(std::move(t)), attrs(std::move(arg_attrs)), id(i), is_managed(ZVal::IsManagedType(type)), tag(type->Tag()) {}

TypeDecl::TypeDecl(const TypeDecl& other) {
    type = other.type;
    attrs = other.attrs;

    id = util::copy_string(other.id);
    is_managed = other.is_managed;
    tag = other.tag;
}

TypeDecl::~TypeDecl() { delete[] id; }

TypeDecl& TypeDecl::operator=(const TypeDecl& other) {
    if ( this == &other )
        return *this;

    type = other.type;
    attrs = other.attrs;

    delete[] id;
    id = util::copy_string(other.id);

    return *this;
}

void TypeDecl::DescribeReST(ODesc* d, bool roles_only) const {
    d->Add(id);
    d->Add(": ");

    if ( ! type->GetName().empty() ) {
        d->Add(":zeek:type:`");
        d->Add(type->GetName());
        d->Add("`");
    }
    else
        type->DescribeReST(d, roles_only);

    if ( attrs ) {
        d->SP();
        attrs->DescribeReST(d);
    }
}

namespace detail {

ExprPtr FieldInit::InitExpr() const { return nullptr; }

// A record field initialization that directly assigns a fixed value ...
class DirectFieldInit final : public FieldInit {
public:
    DirectFieldInit(ZVal _init_val) : init_val(_init_val) {}

    ZVal Generate() const override { return init_val; }

private:
    ZVal init_val;
};

// ... the same, but for a value that needs memory management.
class DirectManagedFieldInit final : public FieldInit {
public:
    DirectManagedFieldInit(ZVal _init_val) : init_val(_init_val) {}
    ~DirectManagedFieldInit() override { ZVal::DeleteManagedType(init_val); }

    ZVal Generate() const override {
        zeek::Ref(init_val.ManagedVal());
        return init_val;
    }

private:
    ZVal init_val;
};

// A record field initialization that's done by evaluating an expression.
class ExprFieldInit final : public FieldInit {
public:
    // Initialization requires evaluating the given expression,
    // yielding a value of the given type (which might require
    // coercion for some records).
    ExprFieldInit(detail::ExprPtr _init_expr, TypePtr _init_type)
        : init_expr(std::move(_init_expr)), init_type(std::move(_init_type)) {
        if ( init_type->Tag() == TYPE_RECORD && ! same_type(init_expr->GetType(), init_type) )
            coerce_type = cast_intrusive<RecordType>(init_type);
    }

    ZVal Generate() const override {
        auto v = eval_in_isolation(init_expr);
        if ( ! v ) {
            reporter->Error("failed &default in record creation");
            return {};
        }

        if ( coerce_type )
            v = v->AsRecordVal()->CoerceTo(coerce_type);

        else if ( init_type->Tag() == TYPE_VECTOR )
            concretize_if_unspecified(cast_intrusive<VectorVal>(v), init_type->Yield());

        return {v, init_type};
    }

    bool IsDeferrable() const override {
        if ( init_expr->Tag() == EXPR_RECORD_CONSTRUCTOR ) {
            // Special-case deferrable record construction.
            auto rce = zeek::cast_intrusive<RecordConstructorExpr>(init_expr);
            auto rt = rce->GetType<zeek::RecordType>();

            // The empty constructor_list check here is a short-cut: If the
            // constructor expression contained only further const expressions
            // or only further deferrable record constructors, this could be
            // more aggressively deferring initializations.
            auto constructor_list = rce->Op();
            return rt->IsDeferrable() && constructor_list->Exprs().empty();
        }

        // Allow deferring fairly common &default=vector() field initialization...
        if ( init_expr->Tag() == EXPR_VECTOR_CONSTRUCTOR ) {
            auto vce = zeek::cast_intrusive<VectorConstructorExpr>(init_expr);
            return vce->GetType<zeek::VectorType>()->IsUnspecifiedVector();
        }

        // ...and also &default=table() and &default=set().
        if ( init_expr->Tag() == EXPR_TABLE_COERCE || init_expr->Tag() == EXPR_TABLE_CONSTRUCTOR ||
             init_expr->Tag() == EXPR_SET_CONSTRUCTOR ) {
            auto une = zeek::cast_intrusive<UnaryExpr>(init_expr);
            return une->Op()->GetType()->Tag() == TYPE_TABLE &&
                   une->Op()->GetType<zeek::TableType>()->IsUnspecifiedTable();
        }

        return false;
    }

    ExprPtr InitExpr() const override { return init_expr; }

private:
    detail::ExprPtr init_expr;
    TypePtr init_type;
    RecordTypePtr coerce_type; // non-nil iff coercion is required
};

// A record field initialization where the field is initialized to an
// empty/default record of the given type.
class RecordFieldInit final : public FieldInit {
public:
    RecordFieldInit(RecordTypePtr _init_type) : init_type(std::move(_init_type)) {}

    ZVal Generate() const override { return {new RecordVal(init_type)}; }

    bool IsDeferrable() const override {
        assert(! run_state::is_parsing);
        return init_type->IsDeferrable();
    }

private:
    RecordTypePtr init_type;
};

// A record field initialization where the field is initialized to an
// empty table of the given type.
class TableFieldInit final : public FieldInit {
public:
    TableFieldInit(TableTypePtr _init_type, detail::AttributesPtr _attrs)
        : init_type(std::move(_init_type)), attrs(std::move(_attrs)) {}

    ZVal Generate() const override { return {new TableVal(init_type, attrs)}; }

private:
    TableTypePtr init_type;
    detail::AttributesPtr attrs;
};

// A record field initialization where the field is initialized to an
// empty vector of the given type.
class VectorFieldInit final : public FieldInit {
public:
    VectorFieldInit(VectorTypePtr _init_type) : init_type(std::move(_init_type)) {}

    ZVal Generate() const override { return {new VectorVal(init_type)}; }

private:
    VectorTypePtr init_type;
};

} // namespace detail

// Helper TraversalCallback optimizing RecordType instances by moving
// deferrable FieldInits from creation_inits to deferred_inits once
// parsing has completed.
class RecordType::CreationInitsOptimizer : public detail::TraversalCallback {
public:
    detail::TraversalCode PreTypedef(const detail::ID* id) override {
        const auto& t = id->GetType();
        if ( analyzed_types.contains(t) )
            return detail::TC_ABORTSTMT;

        analyzed_types.emplace(t);

        if ( t->Tag() == TYPE_RECORD )
            OptimizeCreationInits(t->AsRecordType());

        return detail::TC_CONTINUE;
    }

private:
    void OptimizeCreationInits(RecordType* rt) {
        int i = 0;
        for ( auto& ci : rt->creation_inits ) {
            if ( ! ci.second->IsDeferrable() ) {
                // A non-deferrable field with a &default attribute is expected to also exist in deferred_inits
                // such that re-initialization after deletion of the field works.
                if ( rt->FieldDecl(ci.first)->GetAttr(detail::ATTR_DEFAULT) != detail::Attr::nil ) {
                    if ( ! rt->deferred_inits[ci.first] )
                        zeek::reporter->InternalError("non-deferrable field %s$%s with &default not in deferred_inits",
                                                      rt->GetName().c_str(), rt->FieldName(i));

                    else if ( rt->deferred_inits[ci.first] != ci.second )
                        zeek::reporter->InternalError("non-deferrable field %s$%s with &default has inconsistent inits",
                                                      rt->GetName().c_str(), rt->FieldName(i));
                }

                rt->creation_inits[i++] = std::move(ci);
            }
            else {
                // If deferred_inits already has a value, it should be the same as the one
                // stored in creation_inits. This happens for deferrable record fields
                // that have a &default attribute.
                if ( rt->deferred_inits[ci.first] && rt->deferred_inits[ci.first] != ci.second )
                    zeek::reporter->InternalError("deferrable field %s$%s has inconsistent inits",
                                                  rt->GetName().c_str(), rt->FieldName(i));

                rt->deferred_inits[ci.first].swap(ci.second);
            }
        }

        // Discard remaining elements.
        rt->creation_inits.resize(i);
    }

    // Endless recursion avoidance.
    std::unordered_set<TypePtr> analyzed_types;
};

RecordType::RecordType(type_decl_list* arg_types) : Type(TYPE_RECORD) {
    types = arg_types;

    if ( types ) {
        num_fields = types->length();

        loop_over_list(*types, i) AddField(i, (*types)[i]);
    }
    else
        num_fields = 0;

    num_orig_fields = num_fields;
}

void RecordType::InitPostScript() {
    auto cb = CreationInitsOptimizer();
    detail::traverse_all(&cb);
}

// in this case the clone is actually not so shallow, since
// it gets modified by everyone.
TypePtr RecordType::ShallowClone() {
    auto pass = new type_decl_list();
    for ( const auto& type : *types )
        pass->push_back(new TypeDecl(*type));
    return make_intrusive<RecordType>(pass);
}

RecordType::~RecordType() {
    if ( types ) {
        for ( auto type : *types )
            delete type;

        delete types;
    }
}

void RecordType::AddField(unsigned int field, const TypeDecl* td) {
    ASSERT(field == deferred_inits.size());
    ASSERT(field == managed_fields.size());

    managed_fields.push_back(ZVal::IsManagedType(td->type));

    // We defer error-checking until here so that we can keep deferred_inits
    // and managed_fields correctly tracking the associated fields.

    if ( field_ids.contains(td->id) ) {
        reporter->Error("duplicate field '%s' found in record definition", td->id);
        deferred_inits.push_back(nullptr);
        return;
    }

    field_ids.insert(std::string(td->id));

    auto a = td->attrs;
    auto type = td->type;

    auto def_attr = a ? a->Find(detail::ATTR_DEFAULT) : nullptr;
    auto def_expr = def_attr ? def_attr->GetExpr() : nullptr;

    std::shared_ptr<detail::FieldInit> init;

    if ( def_expr && ! IsErrorType(type->Tag()) ) {
        if ( def_expr->Tag() == detail::EXPR_CONST ) {
            auto zv = ZVal(eval_in_isolation(def_expr), type);

            if ( ZVal::IsManagedType(type) )
                init = std::make_shared<detail::DirectManagedFieldInit>(zv);
            else
                init = std::make_shared<detail::DirectFieldInit>(zv);
        }

        else if ( def_expr->Tag() == detail::EXPR_ARITH_COERCE &&
                  (def_expr->GetOp1()->IsZero() || def_expr->GetOp1()->IsOne()) ) {
            auto zv = ZVal(eval_in_isolation(def_expr), type);
            init = std::make_shared<detail::DirectFieldInit>(zv);
        }

        else {
            // Note that init is placed into creation_inits and deferred_inits
            // such that accessing a record field after deleting it will run the
            // &default expression to re-initialize it again.
            //
            // Also see RecordType::CreationInitsOptimizer.
            init = std::make_shared<detail::ExprFieldInit>(def_expr, type);
            creation_inits.emplace_back(field, init);
        }
    }

    else if ( ! (a && a->Find(detail::ATTR_OPTIONAL)) ) {
        TypeTag tag = type->Tag();

        if ( tag == TYPE_RECORD ) {
            // Initially, put record fields into creation_inits. Once parsing has
            // completed, they may move into deferred_inits. See RecordType::InitPostScript()
            // and RecordType::CreationInitisOptimizer.
            //
            // init (nil) is appended to deferred_inits as placeholder.
            auto rfi = std::make_shared<detail::RecordFieldInit>(cast_intrusive<RecordType>(type));
            creation_inits.emplace_back(field, std::move(rfi));
        }

        else if ( tag == TYPE_TABLE )
            init = std::make_shared<detail::TableFieldInit>(cast_intrusive<TableType>(type), a);

        else if ( tag == TYPE_VECTOR )
            init = std::make_shared<detail::VectorFieldInit>(cast_intrusive<VectorType>(type));
    }

    deferred_inits.push_back(std::move(init));
}

bool RecordType::HasField(const char* field) const { return field_ids.contains(field); }

ValPtr RecordType::FieldDefault(int field) const {
    const TypeDecl* td = FieldDecl(field);

    if ( ! td->attrs )
        return nullptr;

    const auto& def_attr = td->attrs->Find(detail::ATTR_DEFAULT);
    return def_attr ? eval_in_isolation(def_attr->GetExpr()) : nullptr;
}

int RecordType::FieldOffset(const char* field) const {
    loop_over_list(*types, i) {
        TypeDecl* td = (*types)[i];
        if ( util::streq(td->id, field) )
            return i;
    }

    return -1;
}

const char* RecordType::FieldName(int field) const { return FieldDecl(field)->id; }

void RecordType::DoDescribe(ODesc* d) const {
    d->PushType(this);

    if ( d->IsReadable() ) {
        if ( d->IsShort() && ! GetName().empty() )
            d->Add(GetName());

        else {
            d->AddSP("record {");
            DescribeFields(d);
            d->SP();
            d->Add("}");
        }
    }

    else {
        d->Add(static_cast<int>(Tag()));
        DescribeFields(d);
    }

    d->PopType(this);
}

void RecordType::DescribeReST(ODesc* d, bool roles_only) const {
    d->PushType(this);
    d->Add(":zeek:type:`record`");

    if ( num_fields == 0 )
        return;

    d->NL();
    DescribeFieldsReST(d, false);
    d->PopType(this);
}

static string container_type_name(const Type* ft) {
    string s;
    if ( ft->Tag() == TYPE_RECORD )
        s = "record " + ft->GetName();
    else if ( ft->Tag() == TYPE_VECTOR )
        s = "vector of " + container_type_name(ft->Yield().get());
    else if ( ft->Tag() == TYPE_TABLE ) {
        if ( ft->IsSet() )
            s = "set[";
        else
            s = "table[";

        const auto& tl = (static_cast<const IndexType*>(ft))->GetIndexTypes();

        for ( auto i = 0u; i < tl.size(); ++i ) {
            if ( i > 0 )
                s += ',';
            s += container_type_name(tl[i].get());
        }
        s += ']';
        if ( ft->Yield() ) {
            s += " of ";
            s += container_type_name(ft->Yield().get());
        }
    }
    else if ( ft->Tag() == TYPE_ENUM )
        s = "enum " + ft->GetName();
    else
        s = type_name(ft->Tag());
    return s;
}

TableValPtr RecordType::GetRecordFieldsVal(const RecordVal* rv) const {
    static auto record_field = id::find_type<RecordType>("record_field");
    static auto record_field_table = id::find_type<TableType>("record_field_table");

    auto attrs = zeek::make_intrusive<detail::Attributes>(record_field_table,
                                                          /*in_record=*/false,
                                                          /*is_global=*/false);
    attrs->AddAttr(make_intrusive<detail::Attr>(detail::ATTR_ORDERED));
    auto rval = make_intrusive<TableVal>(record_field_table, std::move(attrs));

    for ( int i = 0; i < NumFields(); ++i ) {
        const auto& ft = GetFieldType(i);
        const TypeDecl* fd = FieldDecl(i);
        ValPtr fv;

        if ( rv )
            fv = rv->GetField(i);

        bool logged = (fd->attrs && fd->GetAttr(detail::ATTR_LOG) != nullptr);

        auto nr = make_intrusive<RecordVal>(record_field);

        string s = container_type_name(ft.get());
        nr->Assign(0, s);
        nr->Assign(1, logged);
        nr->Assign(2, std::move(fv));
        nr->Assign(3, FieldDefault(i));
        nr->Assign(4, fd->GetAttr(detail::ATTR_OPTIONAL) != detail::Attr::nil);
        auto field_name = make_intrusive<StringVal>(FieldName(i));
        rval->Assign(std::move(field_name), std::move(nr));
    }

    return rval;
}

const char* RecordType::AddFields(const type_decl_list& others, bool add_log_attr) {
    assert(types);

    bool log = false;

    for ( const auto& td : others ) {
        if ( ! td->GetAttr(detail::ATTR_DEFAULT) && ! td->GetAttr(detail::ATTR_OPTIONAL) )
            return "extension field must be &optional or have &default";
    }

    TableVal::SaveParseTimeTableState(this);

    AddFieldsDirectly(others, add_log_attr);

    RecordVal::ResizeParseTimeRecords(this);
    TableVal::RebuildParseTimeTables();

    return nullptr;
}

void RecordType::AddFieldsDirectly(const type_decl_list& others, bool add_log_attr) {
    for ( const auto& td : others ) {
        if ( add_log_attr ) {
            if ( ! td->attrs )
                td->attrs = make_intrusive<detail::Attributes>(td->type, true, false);

            td->attrs->AddAttr(make_intrusive<detail::Attr>(detail::ATTR_LOG));
        }

        int field = types->size();
        types->push_back(td);
        AddField(field, td);
    }

    num_fields = types->length();
}

void RecordType::DescribeFields(ODesc* d, bool func_args) const {
    if ( d->IsReadable() ) {
        for ( int i = 0; i < num_fields; ++i ) {
            if ( i > 0 )
                d->SP();

            const TypeDecl* td = FieldDecl(i);
            d->Add(td->id);
            d->Add(":");

            if ( d->FindType(td->type.get()) )
                d->Add("<recursion>");
            else
                td->type->Describe(d);

            if ( td->attrs ) {
                d->SP();
                td->attrs->Describe(d);
            }

            if ( func_args ) {
                if ( i + 1 < num_fields )
                    d->Add(",");
            }
            else
                d->Add(";");
        }
    }

    else {
        if ( types ) {
            d->AddCount(0);
            d->AddCount(types->length());
            for ( const auto& type : *types ) {
                d->Add(type->id);
                d->SP();

                if ( d->FindType(type->type.get()) )
                    d->Add("<recursion>");
                else
                    type->type->Describe(d);

                d->SP();
            }
        }
    }
}

void RecordType::DescribeFieldsReST(ODesc* d, bool func_args) const {
    for ( int i = 0; i < num_fields; ++i ) {
        if ( func_args ) {
            if ( i > 0 )
                d->Add(", ");
        }
        else {
            d->NL();
            d->NL();
        }

        const TypeDecl* td = FieldDecl(i);

        if ( d->FindType(td->type.get()) )
            d->Add("<recursion>");
        else {
            if ( num_fields == 1 && util::streq(td->id, "va_args") && td->type->Tag() == TYPE_ANY )
                // This was a BIF using variable argument list
                d->Add("...");
            else {
                if ( func_args ) {
                    td->DescribeReST(d);
                }
                else {
                    // ReST rendering of a TypeDecl as zeek:field directive if
                    // this is about rendering a proper record type and not
                    // function parameters.
                    //
                    // Not sure why we're treating record types and function
                    // signatures the same thing and than denote what it is
                    // by passing around a bool. Open-code the field directive
                    // rendering here.
                    d->Add(".. zeek:field:: ");
                    d->Add(td->id);
                    d->Add(" ");
                    if ( ! td->type->GetName().empty() ) {
                        d->Add(":zeek:type:`");
                        d->Add(td->type->GetName());
                        d->Add("`");
                    }
                    else {
                        td->type->DescribeReST(d, /*roles_only=*/true);
                    }
                    if ( td->attrs ) {
                        d->SP();
                        td->attrs->DescribeReST(d, /*shorten=*/true);
                    }

                    // Good thing ReST doesn't care too much about extra whitespace.
                    d->NL();
                }
            }
        }

        if ( func_args )
            continue;

        zeekygen::detail::IdentifierInfo* doc = detail::zeekygen_mgr->GetIdentifierInfo(GetName());

        if ( ! doc ) {
            reporter->InternalWarning("Failed to lookup record doc: %s", GetName().c_str());
            continue;
        }

        string field_from_script = doc->GetDeclaringScriptForField(td->id);
        string type_from_script;

        if ( doc->GetDeclaringScript() )
            type_from_script = doc->GetDeclaringScript()->Name();

        if ( ! field_from_script.empty() && field_from_script != type_from_script ) {
            d->PushIndent();
            d->Add(zeekygen::detail::redef_indication(field_from_script).c_str());
            d->PopIndent();
        }

        vector<string> cmnts = doc->GetFieldComments(td->id);

        if ( cmnts.empty() )
            continue;

        d->PushIndent();

        for ( size_t i = 0; i < cmnts.size(); ++i ) {
            if ( i > 0 )
                d->NL();

            if ( IsFunc(td->type->Tag()) ) {
                string s = cmnts[i];

                if ( zeekygen::detail::prettify_params(s) )
                    d->NL();

                d->Add(s.c_str());
            }
            else
                d->Add(cmnts[i].c_str());
        }

        d->PopIndent();
    }
}

string RecordType::GetFieldDeprecationWarning(int field, bool has_check) const {
    const TypeDecl* decl = FieldDecl(field);
    if ( decl ) {
        string result;
        if ( const auto& deprecation = decl->GetAttr(detail::ATTR_DEPRECATED) )
            result = deprecation->DeprecationMessage();

        if ( result.empty() )
            return util::fmt("deprecated (%s%s$%s)", GetName().c_str(), has_check ? "?" : "", FieldName(field));
        else
            return util::fmt("deprecated (%s%s$%s): %s", GetName().c_str(), has_check ? "?" : "", FieldName(field),
                             result.c_str());
    }

    return "";
}

detail::TraversalCode RecordType::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    if ( types )
        for ( const auto& td : *types ) {
            tc = td->type->Traverse(cb);
            HANDLE_TC_TYPE_PRE(tc);

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

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

bool RecordType::IsDeferrable() const {
    assert(! run_state::is_parsing);
    auto is_deferrable = [](const auto& p) -> bool { return p.second->IsDeferrable(); };

    // If all creation_inits are deferrable, this record type is deferrable, too.
    // It will be optimized later on. Note, all_of() returns true for an empty
    // range, which is correct.
    return std::ranges::all_of(creation_inits, is_deferrable);
}

FileType::FileType(TypePtr yield_type) : Type(TYPE_FILE), yield(std::move(yield_type)) {}

FileType::~FileType() = default;

void FileType::DoDescribe(ODesc* d) const {
    if ( d->IsReadable() ) {
        d->AddSP("file of");
        yield->Describe(d);
    }
    else {
        d->Add(static_cast<int>(Tag()));
        yield->Describe(d);
    }
}

detail::TraversalCode FileType::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    tc = yield->Traverse(cb);
    HANDLE_TC_TYPE_PRE(tc);

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

OpaqueType::OpaqueType(const string& arg_name) : Type(TYPE_OPAQUE) { name = arg_name; }

void OpaqueType::DoDescribe(ODesc* d) const {
    if ( d->IsReadable() )
        d->AddSP("opaque of");
    else
        d->Add(static_cast<int>(Tag()));

    d->Add(name.c_str());
}

void OpaqueType::DescribeReST(ODesc* d, bool roles_only) const {
    d->Add(util::fmt(":zeek:type:`%s` of %s", type_name(Tag()), name.c_str()));
}

bool OpaqueType::HashSingleValue(const detail::CompositeHash* /*ch*/, detail::HashKey& /*hk*/, const Val* /*v*/,
                                 bool /*type_check*/, bool /*singleton*/) const {
    reporter->InternalError("opaque type '%s' does not support hashing (HashSingleValue)", name.c_str());
    return false;
}

bool OpaqueType::RecoverValFromHash(const detail::CompositeHash* /*ch*/, const detail::HashKey& /*hk*/,
                                    ValPtr* /*pval*/, bool /*singleton*/) const {
    reporter->InternalError("opaque type '%s' does not support hashing (RecoverValFromHash)", name.c_str());
    return false;
}

bool OpaqueType::ReserveHashKeySize(const detail::CompositeHash* /*ch*/, detail::HashKey& /*hk*/, const Val* /*v*/,
                                    bool /*type_check*/, bool /*calc_static_size*/, bool /*singleton*/) const {
    reporter->InternalError("opaque type '%s' does not support hashing (ReserveHashKeySize)", name.c_str());
    return false;
}

ValPtr OpaqueType::CastValueTo(const ValPtr& /*v*/, const Type* /*t*/, std::string& err) const {
    err = "opaque type does not support this cast";
    return nullptr;
}

ValPtr OpaqueType::DefaultVal() const { return nullptr; }

EnumType::EnumType(const string& name) : Type(TYPE_ENUM) {
    counter = 0;
    SetName(name);
}

EnumType::EnumType(const EnumType* e) : Type(TYPE_ENUM), names(e->names), rev_names(e->rev_names), vals(e->vals) {
    counter = e->counter;
    SetName(e->GetName());
}

TypePtr EnumType::ShallowClone() {
    if ( counter == 0 )
        return make_intrusive<EnumType>(GetName());

    return make_intrusive<EnumType>(this);
}

EnumType::~EnumType() = default;

// Note, we use reporter->Error() here (not Error()) to include the current script
// location in the error message, rather than the one where the type was
// originally defined.
void EnumType::AddName(const string& module_name, const char* name, bool is_export, detail::Expr* deprecation,
                       bool from_redef) {
    /* implicit, auto-increment */
    if ( counter < 0 ) {
        reporter->Error("cannot mix explicit enumerator assignment and implicit auto-increment");
        SetError();
        return;
    }
    CheckAndAddName(module_name, name, counter, is_export, deprecation, from_redef);
    counter++;
}

void EnumType::AddName(const string& module_name, const char* name, zeek_int_t val, bool is_export,
                       detail::Expr* deprecation, bool from_redef) {
    /* explicit value specified */
    if ( counter > 0 ) {
        reporter->Error("cannot mix explicit enumerator assignment and implicit auto-increment");
        SetError();
        return;
    }
    counter = -1;
    CheckAndAddName(module_name, name, val, is_export, deprecation, from_redef);
}

void EnumType::CheckAndAddName(const string& module_name, const char* name, zeek_int_t val, bool is_export,
                               detail::Expr* deprecation, bool from_redef) {
    if ( from_redef )
        has_redefs = true;

    if ( Lookup(val) ) {
        reporter->Error("enumerator value in enumerated type definition already exists");
        SetError();
        return;
    }

    if ( val < 0 ) {
        reporter->Error("enumerator value cannot be negative");
        SetError();
        return;
    }

    auto fullname = detail::make_full_var_name(module_name.c_str(), name);
    auto id = id::find(fullname);

    if ( ! id ) {
        id = detail::install_ID(name, module_name.c_str(), true, is_export);
        id->SetType({NewRef{}, this});
        id->SetEnumConst();

        if ( deprecation )
            id->MakeDeprecated({NewRef{}, deprecation});

        detail::zeekygen_mgr->Identifier(id, from_redef);
    }
    else {
        // We allow double-definitions if matching exactly. This is so that
        // we can define an enum both in a *.bif and *.zeek for avoiding
        // cyclic dependencies.
        if ( ! id->IsEnumConst() || (id->HasVal() && val != id->GetVal()->AsEnum()) ||
             GetName() != id->GetType()->GetName() || (names.contains(fullname) && names[fullname] != val) ) {
            auto cl = detail::GetCurrentLocation();
            reporter->PushLocation(&cl, id->GetLocationInfo());
            reporter->Error("conflicting definition of enum value '%s' in type '%s'", fullname.data(),
                            GetName().data());
            reporter->PopLocation();
            SetError();
            return;
        }
    }

    AddNameInternal(module_name, name, val, is_export);

    if ( ! vals.contains(val) )
        vals[val] = make_intrusive<EnumVal>(IntrusivePtr{NewRef{}, this}, val);

    if ( ! id->HasVal() )
        id->SetVal(vals[val]);
    else if ( id->GetVal()->AsEnum() != val )
        reporter->InternalError("inconsistent enum integer value for '%s' (old %" PRId64 " new %" PRId64 ")",
                                fullname.c_str(), id->GetVal()->AsEnum(), val);

    const auto& types = Type::Aliases(GetName());

    for ( const auto& t : types )
        if ( t.get() != this )
            t->AsEnumType()->AddNameInternal(module_name, name, val, is_export);
}

void EnumType::AddNameInternal(const string& module_name, const char* name, zeek_int_t val, bool is_export) {
    string fullname = detail::make_full_var_name(module_name.c_str(), name);
    names[fullname] = val;
    rev_names[val] = std::move(fullname);
}

void EnumType::AddNameInternal(const string& full_name, zeek_int_t val) {
    names[full_name] = val;
    rev_names[val] = full_name;

    if ( ! vals.contains(val) ) {
        if ( counter >= 0 && val == static_cast<zeek_int_t>(vals.size()) )
            ++counter;
        vals[val] = make_intrusive<EnumVal>(IntrusivePtr{NewRef{}, this}, val);
    }
}

zeek_int_t EnumType::Lookup(const string& module_name, const char* name) const {
    return Lookup(detail::make_full_var_name(module_name.c_str(), name));
}

zeek_int_t EnumType::Lookup(const string& full_name) const {
    if ( auto pos = names.find(full_name.c_str()); pos != names.end() )
        return pos->second;

    return -1;
}

const char* EnumType::Lookup(zeek_int_t value) const {
    if ( auto it = rev_names.find(value); it != rev_names.end() )
        return it->second.c_str();

    return nullptr;
}

EnumType::enum_name_list EnumType::Names() const {
    enum_name_list n;
    for ( const auto& [name, value] : names )
        n.emplace_back(name, value);

    return n;
}

const EnumValPtr& EnumType::GetEnumVal(zeek_int_t i) {
    if ( auto it = vals.find(i); it != vals.end() )
        return it->second;

    auto ev = make_intrusive<EnumVal>(IntrusivePtr{NewRef{}, this}, i);
    return vals.emplace(i, std::move(ev)).first->second;
}

void EnumType::DoDescribe(ODesc* d) const {
    auto t = Tag();

    if ( d->IsBinary() ) {
        d->Add(static_cast<int>(t));
        if ( ! d->IsShort() )
            d->Add(GetName());
    }
    else {
        d->Add(type_name(t));
        if ( ! d->IsShort() ) {
            d->SP();
            d->Add(GetName());
        }
    }
}

void EnumType::DescribeReST(ODesc* d, bool roles_only) const {
    d->Add(":zeek:type:`enum`");

    for ( const auto& [_, enum_name] : rev_names ) {
        d->NL();
        d->PushIndent();

        if ( roles_only )
            d->Add(util::fmt(":zeek:enum:`%s`", enum_name.c_str()));
        else
            d->Add(util::fmt(".. zeek:enum:: %s %s", enum_name.c_str(), GetName().c_str()));

        zeekygen::detail::IdentifierInfo* doc = detail::zeekygen_mgr->GetIdentifierInfo(enum_name);

        if ( ! doc ) {
            reporter->InternalWarning("Enum %s documentation lookup failure", enum_name.c_str());
            continue;
        }

        string enum_from_script;
        string type_from_script;

        if ( doc->GetDeclaringScript() )
            enum_from_script = doc->GetDeclaringScript()->Name();

        zeekygen::detail::IdentifierInfo* type_doc = detail::zeekygen_mgr->GetIdentifierInfo(GetName());

        if ( type_doc && type_doc->GetDeclaringScript() )
            type_from_script = type_doc->GetDeclaringScript()->Name();

        if ( ! enum_from_script.empty() && enum_from_script != type_from_script ) {
            d->NL();
            d->PushIndent();
            d->Add(zeekygen::detail::redef_indication(enum_from_script).c_str());
            d->PopIndent();
        }

        vector<string> cmnts = doc->GetComments();

        if ( cmnts.empty() ) {
            d->PopIndentNoNL();
            continue;
        }

        d->NL();
        d->PushIndent();

        for ( size_t i = 0; i < cmnts.size(); ++i ) {
            if ( i > 0 )
                d->NL();

            d->Add(cmnts[i].c_str());
        }

        d->PopIndentNoNL();
        d->PopIndentNoNL();
    }
}

VectorType::VectorType(TypePtr element_type) : Type(TYPE_VECTOR), yield_type(std::move(element_type)) {}

TypePtr VectorType::ShallowClone() { return make_intrusive<VectorType>(yield_type); }

VectorType::~VectorType() = default;

const TypePtr& VectorType::Yield() const {
    // Work around the fact that we use void internally to mark a vector
    // as being unspecified. When looking at its yield type, we need to
    // return any as that's what other code historically expects for type
    // comparisons.
    if ( IsUnspecifiedVector() )
        return zeek::base_type(TYPE_ANY);

    return yield_type;
}

int VectorType::MatchesIndex(detail::ListExpr* const index) const {
    ExprPList& el = index->Exprs();

    if ( el.length() != 1 && el.length() != 2 )
        return DOES_NOT_MATCH_INDEX;

    if ( el.length() == 2 )
        return MATCHES_INDEX_VECTOR;
    else if ( el[0]->GetType()->Tag() == TYPE_VECTOR )
        return (IsIntegral(el[0]->GetType()->Yield()->Tag()) || IsBool(el[0]->GetType()->Yield()->Tag())) ?
                   MATCHES_INDEX_VECTOR :
                   DOES_NOT_MATCH_INDEX;
    else
        return (IsIntegral(el[0]->GetType()->Tag()) || IsBool(el[0]->GetType()->Tag())) ? MATCHES_INDEX_SCALAR :
                                                                                          DOES_NOT_MATCH_INDEX;
}

bool VectorType::IsUnspecifiedVector() const { return yield_type->Tag() == TYPE_VOID; }

void VectorType::DoDescribe(ODesc* d) const {
    if ( IsUnspecifiedVector() && d->IsReadable() ) {
        d->Add("vector()");
        return;
    }

    if ( d->IsReadable() )
        d->AddSP("vector of");
    else
        d->Add(static_cast<int>(Tag()));

    yield_type->Describe(d);
}

void VectorType::DescribeReST(ODesc* d, bool roles_only) const {
    d->Add(util::fmt(":zeek:type:`%s` of ", type_name(Tag())));

    if ( yield_type->GetName().empty() )
        yield_type->DescribeReST(d, roles_only);
    else
        d->Add(util::fmt(":zeek:type:`%s`", yield_type->GetName().c_str()));
}

detail::TraversalCode VectorType::Traverse(detail::TraversalCallback* cb) const {
    auto tc = cb->PreType(this);
    HANDLE_TC_TYPE_PRE(tc);

    tc = yield_type->Traverse(cb);
    HANDLE_TC_TYPE_PRE(tc);

    tc = cb->PostType(this);
    HANDLE_TC_TYPE_POST(tc);
}

// Returns true if t1 is initialization-compatible with t2 (i.e., if an
// initializer with type t1 can be used to initialize a value with type t2),
// false otherwise.  Assumes that t1's tag is different from t2's.  Note
// that the test is in only one direction - we don't check whether t2 is
// initialization-compatible with t1.
static bool is_init_compat(const Type& t1, const Type& t2) {
    if ( t1.Tag() == TYPE_LIST ) {
        if ( t2.Tag() == TYPE_RECORD )
            return true;
        else
            return t1.AsTypeList()->AllMatch(&t2, true);
    }

    if ( t1.IsSet() )
        return same_type(*t1.AsTableType()->GetIndices(), t2, true);

    return false;
}

bool same_type(const Type& arg_t1, const Type& arg_t2, bool is_init, bool match_record_field_names) {
    if ( &arg_t1 == &arg_t2 || arg_t1.Tag() == TYPE_ANY || arg_t2.Tag() == TYPE_ANY )
        return true;

    auto t1 = &arg_t1;
    auto t2 = &arg_t2;

    if ( t1->Tag() != t2->Tag() ) {
        if ( is_init )
            return is_init_compat(*t1, *t2) || is_init_compat(*t2, *t1);

        return false;
    }

    // A major complication we have to deal with is the potential
    // presence of recursive types (records, in particular).  If
    // we simply traverse a type's members recursively, then if the
    // type is itself recursive we will end up with infinite recursion.
    // To prevent this, we need to instead track our analysis process

    // Which types we're in the process of analyzing.  We add (compound)
    // types to this as we recurse into their elements, and remove them
    // when we're done processing them.
    static std::unordered_set<const Type*> analyzed_types;

    // First do all checks that don't require any recursion.

    switch ( t1->Tag() ) {
        case TYPE_VOID: // NOLINT(bugprone-branch-clone)
        case TYPE_BOOL:
        case TYPE_INT:
        case TYPE_COUNT:
        case TYPE_DOUBLE:
        case TYPE_TIME:
        case TYPE_INTERVAL:
        case TYPE_STRING:
        case TYPE_PATTERN:
        case TYPE_PORT:
        case TYPE_ADDR:
        case TYPE_SUBNET:
        case TYPE_ANY:
        case TYPE_ERROR: return true;

        case TYPE_ENUM: {
            // We have enum types with the same name, but different pointers.
            // This comes from situations where the same enum is defined within
            // a .bif file and a .zeek file. Supervisor::ClusterRole and
            // Broker::BackendType are examples.
            //
            // It'd be nice if enum types were singletons instead.
            if ( t1->GetName() == t2->GetName() )
                return true;

            // If an EnumType has a parent and the type to compare
            // against is that parent, they are compatible...
            const auto* t1p = t1->AsEnumType()->GetParentType().get();
            const auto* t2p = t2->AsEnumType()->GetParentType().get();

            if ( t1p && t1p == t2 )
                return true;

            if ( t2p && t2p == t1 )
                return true;

            // Remove in v9.1: Make enums nominally typed. Change trailing
            // return to false.
            //
            // We only output warnings during parse time for the user to see
            // when we return true for nominally different enum types. I'm a
            // bit worried we may somehow get here at runtime and spill a lot
            // of warnings unexpectedly.
            if ( run_state::is_parsing )
                reporter->Deprecation(
                    util::fmt("Remove in v9.1. Mixing incompatible enum types %s and %s will become an error.",
                              obj_desc_short(t1).c_str(), obj_desc_short(t2).c_str()));

            // Remove in v9.1: Change to return false.
            return true;
        }

        case TYPE_OPAQUE: {
            const OpaqueType* ot1 = static_cast<const OpaqueType*>(t1);
            const OpaqueType* ot2 = static_cast<const OpaqueType*>(t2);
            return ot1->Name() == ot2->Name();
        }

        case TYPE_TABLE: {
            const IndexType* it1 = static_cast<const IndexType*>(t1);
            const IndexType* it2 = static_cast<const IndexType*>(t2);

            const auto& tl1 = it1->GetIndices();
            const auto& tl2 = it2->GetIndices();

            if ( (tl1 || tl2) && ! (tl1 && tl2) )
                return false;

            // If one is a set and one isn't, they shouldn't
            // be considered the same type.
            if ( t1->IsSet() != t2->IsSet() )
                return false;

            const auto& y1 = t1->Yield();
            const auto& y2 = t2->Yield();

            if ( (y1 || y2) && ! (y1 && y2) )
                return false;

            break;
        }

        case TYPE_FUNC: {
            const FuncType* ft1 = static_cast<const FuncType*>(t1);
            const FuncType* ft2 = static_cast<const FuncType*>(t2);

            if ( ft1->Flavor() != ft2->Flavor() )
                return false;

            const auto& y1 = t1->Yield();
            const auto& y2 = t2->Yield();
            if ( (y1 || y2) && ! (y1 && y2) )
                return false;

            break;
        }

        case TYPE_RECORD: {
            const RecordType* rt1 = static_cast<const RecordType*>(t1);
            const RecordType* rt2 = static_cast<const RecordType*>(t2);

            if ( rt1->NumFields() != rt2->NumFields() )
                return false;

            for ( int i = 0; i < rt1->NumFields(); ++i ) {
                const TypeDecl* td1 = rt1->FieldDecl(i);
                const TypeDecl* td2 = rt2->FieldDecl(i);

                if ( match_record_field_names && ! util::streq(td1->id, td2->id) )
                    return false;

                if ( ! same_attrs(td1->attrs.get(), td2->attrs.get()) )
                    return false;
            }

            break;
        }

        case TYPE_LIST: {
            const auto& tl1 = t1->AsTypeList()->GetTypes();
            const auto& tl2 = t2->AsTypeList()->GetTypes();

            if ( tl1.size() != tl2.size() )
                return false;

            break;
        }

        case TYPE_VECTOR:
        case TYPE_FILE:
        case TYPE_TYPE: break;
    }

    // If we get to here, then we're dealing with a type with
    // subtypes, and thus potentially recursive.

    if ( analyzed_types.contains(t1) || analyzed_types.contains(t2) ) {
        // We've analyzed at least one of the types previously.
        // Avoid infinite recursion.

        if ( analyzed_types.contains(t1) && analyzed_types.contains(t2) )
            // We've analyzed them both.  In theory, this
            // could happen while the types are still different.
            // Checking for that is a pain - we could do so
            // by recursively expanding all of the types present
            // when traversing them (suppressing repeats), and
            // see that they individually match in a non-recursive
            // manner.  For now, we assume they're a direct match.
            return true;

        // One is definitely recursive and the other has not yet
        // manifested as such.  In theory, they again could still
        // be a match, if the non-recursive one would manifest
        // becoming recursive if only we traversed it further, but
        // for now we assume they're not a match.
        return false;
    }

    // Track the two types for when we recurse.
    analyzed_types.insert(t1);
    analyzed_types.insert(t2);

    bool result;

    switch ( t1->Tag() ) {
        case TYPE_TABLE: {
            const IndexType* it1 = static_cast<const IndexType*>(t1);
            const IndexType* it2 = static_cast<const IndexType*>(t2);

            const auto& tl1 = it1->GetIndices();
            const auto& tl2 = it2->GetIndices();

            if ( ! same_type(tl1, tl2, is_init, match_record_field_names) )
                result = false;
            else if ( t1->IsSet() && t2->IsSet() )
                // Sets don't have yield types because they don't have values. If
                // both types are sets, and we already matched on the indices
                // above consider that a success. We already checked the case
                // where only one of the two is a set earlier.
                result = true;
            else {
                const auto& y1 = t1->Yield();
                const auto& y2 = t2->Yield();

                result = same_type(y1, y2, is_init, match_record_field_names);
            }
            break;
        }

        case TYPE_FUNC: {
            const FuncType* ft1 = static_cast<const FuncType*>(t1);
            const FuncType* ft2 = static_cast<const FuncType*>(t2);

            if ( ! same_type(t1->Yield(), t2->Yield(), is_init, match_record_field_names) )
                result = false;
            else
                result = ft1->CheckArgs(ft2->ParamList()->GetTypes(), is_init, false);
            break;
        }

        case TYPE_RECORD: {
            const RecordType* rt1 = static_cast<const RecordType*>(t1);
            const RecordType* rt2 = static_cast<const RecordType*>(t2);

            result = true;

            for ( int i = 0; i < rt1->NumFields(); ++i ) {
                const TypeDecl* td1 = rt1->FieldDecl(i);
                const TypeDecl* td2 = rt2->FieldDecl(i);

                if ( ! same_type(td1->type, td2->type, is_init, match_record_field_names) ) {
                    result = false;
                    break;
                }
            }
            break;
        }

        case TYPE_LIST: {
            const auto& tl1 = t1->AsTypeList()->GetTypes();
            const auto& tl2 = t2->AsTypeList()->GetTypes();

            result = true;

            for ( auto i = 0u; i < tl1.size(); ++i )
                if ( ! same_type(tl1[i], tl2[i], is_init, match_record_field_names) ) {
                    result = false;
                    break;
                }
            break;
        }

        case TYPE_VECTOR:
        case TYPE_FILE: result = same_type(t1->Yield(), t2->Yield(), is_init, match_record_field_names); break;

        case TYPE_TYPE: {
            auto tt1 = t1->AsTypeType();
            auto tt2 = t2->AsTypeType();
            result = same_type(tt1->GetType(), tt1->GetType(), is_init, match_record_field_names);
            break;
        }

        default: result = false;
    }

    analyzed_types.erase(t1);
    analyzed_types.erase(t2);

    return result;
}

bool same_attrs(const detail::Attributes* a1, const detail::Attributes* a2) {
    if ( ! a1 )
        return (a2 == nullptr);

    if ( ! a2 )
        return (a1 == nullptr);

    return (*a1 == *a2);
}

bool record_promotion_compatible(const RecordType* super_rec, const RecordType* sub_rec) {
    for ( int i = 0; i < sub_rec->NumFields(); ++i ) {
        int o = super_rec->FieldOffset(sub_rec->FieldName(i));

        if ( o < 0 )
            // Orphaned field.
            continue;

        const auto& sub_field_type = sub_rec->GetFieldType(i);
        const auto& super_field_type = super_rec->GetFieldType(o);

        if ( same_type(sub_field_type, super_field_type) )
            continue;

        if ( BothArithmetic(sub_field_type->Tag(), super_field_type->Tag()) )
            continue;

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

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

        if ( ! record_promotion_compatible(super_field_type->AsRecordType(), sub_field_type->AsRecordType()) )
            return false;
    }

    return true;
}

const Type* flatten_type(const Type* t) {
    if ( t->Tag() != TYPE_LIST )
        return t;

    const TypeList* tl = t->AsTypeList();

    if ( tl->IsPure() )
        return tl->GetPureType().get();

    const auto& types = tl->GetTypes();

    if ( types.empty() )
        reporter->InternalError("empty type list in flatten_type");

    const auto& ft = types[0];

    if ( types.size() == 1 || tl->AllMatch(ft, false) )
        return ft.get();

    return t;
}

Type* flatten_type(Type* t) { return const_cast<Type*>(flatten_type(static_cast<const Type*>(t))); }

bool is_assignable(TypeTag t) {
    switch ( t ) {
        case TYPE_BOOL:
        case TYPE_INT:
        case TYPE_COUNT:
        case TYPE_DOUBLE:
        case TYPE_TIME:
        case TYPE_INTERVAL:
        case TYPE_STRING:
        case TYPE_PATTERN:
        case TYPE_ENUM:
        case TYPE_PORT:
        case TYPE_ADDR:
        case TYPE_SUBNET:
        case TYPE_RECORD:
        case TYPE_FUNC:
        case TYPE_ANY:
        case TYPE_ERROR:
        case TYPE_LIST:
        case TYPE_VECTOR:
        case TYPE_FILE:
        case TYPE_OPAQUE:
        case TYPE_TABLE:
        case TYPE_TYPE: return true;

        case TYPE_VOID: return false;
    }

    return false;
}

TypeTag max_type(TypeTag t1, TypeTag t2) {
    if ( t1 == TYPE_INTERVAL || t1 == TYPE_TIME )
        t1 = TYPE_DOUBLE;
    if ( t2 == TYPE_INTERVAL || t2 == TYPE_TIME )
        t2 = TYPE_DOUBLE;

    if ( BothArithmetic(t1, t2) ) {
        if ( t1 == TYPE_DOUBLE || t2 == TYPE_DOUBLE )
            return TYPE_DOUBLE;
        else if ( t1 == TYPE_INT || t2 == TYPE_INT )
            return TYPE_INT;
        else if ( t1 == TYPE_COUNT || t2 == TYPE_COUNT )
            return TYPE_COUNT;

        return TYPE_COUNT;
    }
    else {
        reporter->InternalError("non-arithmetic tags in max_type()");
        return TYPE_ERROR;
    }
}

static TypePtr merge_enum_types(const Type* t1, const Type* t2) {
    // Could compare pointers t1 == t2, but maybe there's someone out
    // there creating clones of the type, so safer to compare name.
    if ( t1->GetName() != t2->GetName() ) {
        std::string msg =
            util::fmt("incompatible enum types: '%s' and '%s'", t1->GetName().data(), t2->GetName().data());

        t1->Error(msg.data(), t2);
        return nullptr;
    }

    // Doing a lookup here as a roundabout way of ref-ing t1, without
    // changing the function params which has t1 as const and also
    // (potentially) avoiding a pitfall mentioned earlier about clones.
    const auto& id = detail::global_scope()->Find(t1->GetName());

    if ( id && id->IsType() && id->GetType()->Tag() == TYPE_ENUM )
        // It should make most sense to return the real type here rather
        // than a copy since it may be redef'd later in parsing.  If we
        // return a copy, then whoever is using this return value won't
        // actually see those changes from the redef.
        return id->GetType();

    std::string msg = util::
        fmt("incompatible enum types: '%s' and '%s'"
            " ('%s' enum type ID is invalid)",
            t1->GetName().data(), t2->GetName().data(), t1->GetName().data());
    t1->Error(msg.data(), t2);
    return nullptr;
}

static TypePtr merge_table_types(const Type* t1, const Type* t2) {
    auto it1 = t1->AsTableType();
    auto it2 = t2->AsTableType();

    const auto& tl1 = it1->GetIndexTypes();
    const auto& tl2 = it2->GetIndexTypes();
    const auto& y1 = it1->Yield();
    const auto& y2 = it2->Yield();

    if ( it1->IsUnspecifiedTable() )
        return make_intrusive<TableType>(it2->GetIndices(), y2);
    if ( it2->IsUnspecifiedTable() )
        return make_intrusive<TableType>(it1->GetIndices(), y1);

    TypeListPtr tl3;

    if ( tl1.size() != tl2.size() ) {
        t1->Error("incompatible types", t2);
        return nullptr;
    }

    tl3 = make_intrusive<TypeList>();

    for ( auto i = 0u; i < tl1.size(); ++i ) {
        auto tl3_i = merge_types(tl1[i], tl2[i]);
        if ( ! tl3_i )
            return nullptr;

        tl3->Append(std::move(tl3_i));
    }

    TypePtr y3;

    if ( y1 || y2 ) {
        if ( ! y1 || ! y2 ) {
            t1->Error("incompatible types", t2);
            return nullptr;
        }

        y3 = merge_types(y1, y2);
        if ( ! y3 )
            return nullptr;
    }

    return make_intrusive<TableType>(std::move(tl3), std::move(y3));
}

static TypePtr merge_func_types(const Type* t1, const Type* t2) {
    if ( ! same_type(t1, t2) ) {
        t1->Error("incompatible types", t2);
        return nullptr;
    }

    const FuncType* ft1 = static_cast<const FuncType*>(t1);
    const FuncType* ft2 = static_cast<const FuncType*>(t1);
    auto args = cast_intrusive<RecordType>(merge_types(ft1->Params(), ft2->Params()));
    auto yield = t1->Yield() ? merge_types(t1->Yield(), t2->Yield()) : nullptr;

    return make_intrusive<FuncType>(std::move(args), std::move(yield), ft1->Flavor());
}

static TypePtr merge_record_types(const Type* t1, const Type* t2) {
    const RecordType* rt1 = static_cast<const RecordType*>(t1);
    const RecordType* rt2 = static_cast<const RecordType*>(t2);

    // We allow the records to have different numbers of fields.
    // We first go through all of the fields in rt1, and then we
    // check for whether rt2 has any additional fields.

    type_decl_list* tdl3 = new type_decl_list();

    for ( int i = 0; i < rt1->NumFields(); ++i ) {
        auto td1 = rt1->FieldDecl(i);
        auto td2_offset_i = rt2->FieldOffset(rt1->FieldName(i));

        TypePtr tdl3_i;
        auto attrs3 = make_intrusive<detail::Attributes>(nullptr, true, false);

        if ( td1->attrs )
            attrs3->AddAttrs(td1->attrs);

        if ( td2_offset_i >= 0 ) {
            auto td2 = rt2->FieldDecl(td2_offset_i);
            tdl3_i = merge_types(td1->type, td2->type);

            if ( td2->attrs )
                attrs3->AddAttrs(td2->attrs);

            if ( ! util::streq(td1->id, td2->id) || ! tdl3_i ) {
                t1->Error("incompatible record fields", t2);
                delete tdl3;
                return nullptr;
            }
        }
        else {
            tdl3_i = td1->type;
            attrs3->AddAttr(make_intrusive<detail::Attr>(detail::ATTR_OPTIONAL));
        }

        if ( attrs3->GetAttrs().empty() )
            attrs3 = nullptr;

        auto td3 = new TypeDecl(util::copy_string(td1->id), std::move(tdl3_i), attrs3);

        tdl3->push_back(td3);
    }

    // Now add in any extras from rt2.
    for ( int i = 0; i < rt2->NumFields(); ++i ) {
        auto td2 = rt2->FieldDecl(i);
        auto td1_offset_i = rt1->FieldOffset(rt2->FieldName(i));

        if ( td1_offset_i < 0 ) {
            auto attrs3 = make_intrusive<detail::Attributes>(nullptr, true, false);
            if ( td2->attrs )
                attrs3->AddAttrs(td2->attrs);

            attrs3->AddAttr(make_intrusive<detail::Attr>(detail::ATTR_OPTIONAL));
            auto td_merge = new TypeDecl(util::copy_string(td2->id), td2->type, attrs3);
            tdl3->push_back(td_merge);
        }
    }

    return make_intrusive<RecordType>(tdl3);
}

static TypePtr merge_vector_types(const Type* t1, const Type* t2) {
    auto vt1 = t1->AsVectorType();
    auto vt2 = t2->AsVectorType();

    auto y1 = vt1->Yield();
    auto y2 = vt2->Yield();

    if ( vt1->IsUnspecifiedVector() )
        return make_intrusive<VectorType>(y2);
    if ( vt2->IsUnspecifiedVector() )
        return make_intrusive<VectorType>(y1);

    if ( ! same_type(y1, y2) ) {
        t1->Error("incompatible types", t2);
        return nullptr;
    }

    return make_intrusive<VectorType>(merge_types(y1, y2));
}

static TypeListPtr merge_list_types(const Type* t1, const Type* t2) {
    const TypeList* tl1 = t1->AsTypeList();
    const TypeList* tl2 = t2->AsTypeList();

    if ( tl1->IsPure() != tl2->IsPure() ) {
        tl1->Error("incompatible lists", tl2);
        return nullptr;
    }

    const auto& l1 = tl1->GetTypes();
    const auto& l2 = tl2->GetTypes();

    if ( l1.empty() || l2.empty() ) {
        if ( l1.empty() )
            tl1->Error("empty list");
        else
            tl2->Error("empty list");
        return nullptr;
    }

    if ( l1.size() != l2.size() ) {
        tl1->Error("different number of indices", tl2);
        return nullptr;
    }

    auto tl3 = make_intrusive<TypeList>();

    for ( auto i = 0u; i < l1.size(); ++i ) {
        auto mt = merge_types(l1[i], l2[i]);
        if ( ! mt )
            return nullptr;

        tl3->Append(std::move(mt));
    }

    return tl3;
}

TypePtr merge_types(const TypePtr& arg_t1, const TypePtr& arg_t2) {
    if ( arg_t1 == arg_t2 )
        return arg_t1;

    auto t1 = arg_t1.get();
    auto t2 = arg_t2.get();
    // t1 = flatten_type(t1);
    // t2 = flatten_type(t2);

    TypeTag tg1 = t1->Tag();
    TypeTag tg2 = t2->Tag();

    if ( BothArithmetic(tg1, tg2) )
        return base_type(max_type(tg1, tg2));

    if ( tg1 != tg2 ) {
        t1->Error("incompatible types", t2);
        return nullptr;
    }

    switch ( tg1 ) {
        case TYPE_TIME:
        case TYPE_INTERVAL:
        case TYPE_STRING:
        case TYPE_PATTERN:
        case TYPE_PORT:
        case TYPE_ADDR:
        case TYPE_SUBNET:
        case TYPE_BOOL:
        case TYPE_ANY:
        case TYPE_ERROR: return base_type(tg1);

        case TYPE_ENUM: return merge_enum_types(t1, t2);

        case TYPE_TABLE: return merge_table_types(t1, t2);

        case TYPE_FUNC: return merge_func_types(t1, t2);

        case TYPE_RECORD: return merge_record_types(t1, t2);

        case TYPE_LIST: return merge_list_types(t1, t2);

        case TYPE_VECTOR: return merge_vector_types(t1, t2);

        case TYPE_FILE:
            if ( ! same_type(t1->Yield(), t2->Yield()) ) {
                t1->Error("incompatible types", t2);
                return nullptr;
            }

            return make_intrusive<FileType>(merge_types(t1->Yield(), t2->Yield()));

        default: reporter->InternalError("bad type in merge_types()"); return nullptr;
    }
}

TypePtr maximal_type(detail::ListExpr* elements) {
    TypeList* tl_type = elements->GetType()->AsTypeList();
    const auto& tl = tl_type->GetTypes();

    if ( tl.empty() ) {
        reporter->Error("no type can be inferred for empty list");
        return nullptr;
    }

    auto t = tl[0];

    if ( tl.size() == 1 )
        return t;

    for ( size_t i = 1; t && i < tl.size(); ++i ) {
        const auto& tl_i = tl[i];

        if ( t == tl_i )
            continue;

        if ( BothArithmetic(t->Tag(), tl_i->Tag()) )
            t = merge_types(t, tl_i);

        else if ( t->Tag() == TYPE_ENUM && tl_i->Tag() == TYPE_ENUM )
            t = merge_enum_types(t.get(), tl_i.get());

        else if ( ! same_type(t, tl_i) )
            t = nullptr;
    }

    if ( ! t )
        reporter->Error("inconsistent types in list");

    return t;
}

// Reduces an aggregate type.
static Type* reduce_type(Type* t) {
    if ( t->Tag() == TYPE_LIST )
        return flatten_type(t);

    else if ( t->IsSet() ) {
        const auto& tl = t->AsTableType()->GetIndices();

        if ( tl->GetTypes().size() == 1 )
            return tl->GetTypes()[0].get();
        else
            return tl.get();
    }

    else
        return t;
}

static TableTypePtr init_table_type(detail::ListExpr* l) {
    auto& elems = l->Exprs();
    TypePtr index;
    TypePtr yield;

    for ( auto e : elems ) {
        if ( e->Tag() != detail::EXPR_ASSIGN ) {
            e->Error("table constructor element lacks '=' structure");
            return nullptr;
        }

        auto& ind = e->GetOp1()->GetType();
        auto& y = e->GetOp2()->GetType();

        if ( ! index ) {
            index = ind;
            yield = y;
            continue;
        }

        index = merge_types(index, ind);
        yield = merge_types(yield, y);

        if ( ! index || ! yield )
            // Error message already generated.
            return nullptr;
    }

    if ( index->Tag() != TYPE_LIST )
        return nullptr;

    return make_intrusive<TableType>(cast_intrusive<TypeList>(index), yield);
}

static TableTypePtr init_set_type(detail::ListExpr* l) {
    auto& elems = l->Exprs();
    TypePtr index;

    for ( auto e : elems ) {
        auto& ind = e->GetType();

        if ( ! index ) {
            index = ind;
            continue;
        }

        index = merge_types(index, ind);

        if ( ! index )
            return nullptr;
    }

    TypeListPtr ind_list;

    if ( index->Tag() == TYPE_LIST )
        ind_list = cast_intrusive<TypeList>(index);
    else {
        ind_list = make_intrusive<TypeList>(index);
        ind_list->Append(index);
    }

    return make_intrusive<TableType>(ind_list, nullptr);
}

TypePtr init_type(const detail::ExprPtr& init) {
    if ( init->Tag() != detail::EXPR_LIST ) {
        auto t = init->InitType();

        if ( ! t ) {
            init->Error("not a valid initializer");
            return nullptr;
        }

        if ( (t->Tag() == TYPE_TABLE && cast_intrusive<TableType>(t)->IsUnspecifiedTable()) ||
             (t->Tag() == TYPE_VECTOR && cast_intrusive<VectorType>(t)->IsUnspecifiedVector()) ) {
            init->Error("empty constructor in untyped initialization");
            return nullptr;
        }

        return t;
    }

    auto init_list = init->AsListExpr();
    const auto& el = init_list->Exprs();

    if ( el.empty() ) {
        init->Error("empty list in untyped initialization");
        return nullptr;
    }

    // Could be a record, a set, or a list of table elements.
    auto e0 = el[0];

    if ( e0->IsRecordElement(nullptr) )
        // ListExpr's know how to build a record from their components.
        return init_list->InitType();

    if ( e0->Tag() == detail::EXPR_ASSIGN )
        return init_table_type(init_list);
    else
        return init_set_type(init_list);
}

bool is_atomic_type(const Type& t) {
    switch ( t.InternalType() ) {
        case TYPE_INTERNAL_INT:
        case TYPE_INTERNAL_UNSIGNED:
        case TYPE_INTERNAL_DOUBLE:
        case TYPE_INTERNAL_STRING:
        case TYPE_INTERNAL_ADDR:
        case TYPE_INTERNAL_SUBNET: return true;
        default: return false;
    }
}

const TypePtr& base_type(TypeTag tag) {
    static TypePtr base_types[NUM_TYPES];

    if ( tag >= NUM_TYPES )
        reporter->InternalError("base_type() called with invalid tag %d", tag);

    if ( ! base_types[tag] ) {
        base_types[tag] = make_intrusive<Type>(tag, true);
        // Give the base types a pseudo-location for easier identification.
        detail::Location l(type_name(tag), 0, 0);
        base_types[tag]->SetLocationInfo(&l);
    }

    return base_types[tag];
}

} // namespace zeek
