set_var_const(
    char_u	*name,
    scid_T	sid,
    type_T	*type_arg,
    typval_T	*tv_arg,
    int		copy,	    // make copy of value in "tv"
    int		flags_arg,  // ASSIGN_CONST, ASSIGN_FINAL, etc.
    int		var_idx)    // index for ":let [a, b] = list"
{
    typval_T	*tv = tv_arg;
    type_T	*type = type_arg;
    typval_T	bool_tv;
    dictitem_T	*di;
    typval_T	*dest_tv = NULL;
    char_u	*varname;
    char_u	*name_tofree = NULL;
    hashtab_T	*ht = NULL;
    int		is_script_local;
    int		vim9script = in_vim9script();
    int		var_in_vim9script;
    int		var_in_autoload = FALSE;
    int		flags = flags_arg;
    int		free_tv_arg = !copy;  // free tv_arg if not used

    if (sid != 0)
    {
	if (SCRIPT_ID_VALID(sid))
	    ht = &SCRIPT_VARS(sid);
	varname = name;
    }
    else
    {
	scriptitem_T *si;

	if (in_vim9script() && is_export
		&& SCRIPT_ID_VALID(current_sctx.sc_sid)
		&& (si = SCRIPT_ITEM(current_sctx.sc_sid))
						   ->sn_autoload_prefix != NULL)
	{
	    // In a vim9 autoload script an exported variable is put in the
	    // global namespace with the autoload prefix.
	    var_in_autoload = TRUE;
	    varname = concat_str(si->sn_autoload_prefix, name);
	    if (varname == NULL)
		goto failed;
	    name_tofree = varname;
	    ht = &globvarht;
	}
	else
	    ht = find_var_ht(name, &varname);
    }
    if (ht == NULL || *varname == NUL)
    {
	semsg(_(e_illegal_variable_name_str), name);
	goto failed;
    }
    is_script_local = ht == get_script_local_ht() || sid != 0
							    || var_in_autoload;

    if (vim9script
	    && !is_script_local
	    && (flags & (ASSIGN_NO_DECL | ASSIGN_DECL)) == 0
	    && (flags & (ASSIGN_CONST | ASSIGN_FINAL)) == 0
	    && name[1] == ':')
    {
	vim9_declare_error(name);
	goto failed;
    }
    if ((flags & ASSIGN_FOR_LOOP) && name[1] == ':'
			      && vim_strchr((char_u *)"gwbt", name[0]) != NULL)
	// Do not make g:var, w:var, b:var or t:var final.
	flags &= ~ASSIGN_FINAL;

    var_in_vim9script = is_script_local && current_script_is_vim9();
    if (var_in_vim9script && name[0] == '_' && name[1] == NUL)
    {
	// For "[a, _] = list" the underscore is ignored.
	if ((flags & ASSIGN_UNPACK) == 0)
	    emsg(_(e_cannot_use_underscore_here));
	goto failed;
    }

    di = find_var_in_ht(ht, 0, varname, TRUE);

    if (di == NULL && var_in_vim9script)
    {
	imported_T  *import = find_imported(varname, 0, FALSE);

	if (import != NULL)
	{
	    // imported name space cannot be used
	    if ((flags & ASSIGN_NO_DECL) == 0)
	    {
		semsg(_(e_redefining_imported_item_str), name);
		goto failed;
	    }
	    semsg(_(e_cannot_use_str_itself_it_is_imported), name);
	    goto failed;
	}
	if (!in_vim9script())
	{
	    semsg(_(e_cannot_create_vim9_script_variable_in_function_str),
									 name);
	    goto failed;
	}
    }

    if (dest_tv == NULL)
    {
	// Search in parent scope which is possible to reference from lambda
	if (di == NULL)
	    di = find_var_in_scoped_ht(name, TRUE);

	if ((tv->v_type == VAR_FUNC || tv->v_type == VAR_PARTIAL)
				      && var_wrong_func_name(name, di == NULL))
	    goto failed;

	if (need_convert_to_bool(type, tv))
	{
	    // Destination is a bool and the value is not, but it can be
	    // converted.
	    CLEAR_FIELD(bool_tv);
	    bool_tv.v_type = VAR_BOOL;
	    bool_tv.vval.v_number = tv2bool(tv) ? VVAL_TRUE : VVAL_FALSE;
	    tv = &bool_tv;
	}

	if (di != NULL)
	{
	    // Item already exists.  Allowed to replace when reloading.
	    if ((di->di_flags & DI_FLAGS_RELOAD) == 0)
	    {
		if ((flags & (ASSIGN_CONST | ASSIGN_FINAL))
					     && (flags & ASSIGN_FOR_LOOP) == 0)
		{
		    emsg(_(e_cannot_modify_existing_variable));
		    goto failed;
		}

		if (is_script_local && vim9script
			      && (flags & (ASSIGN_NO_DECL | ASSIGN_DECL)) == 0)
		{
		    semsg(_(e_redefining_script_item_str), name);
		    goto failed;
		}

		if (var_in_vim9script && (flags & ASSIGN_FOR_LOOP) == 0)
		{
		    where_T where = WHERE_INIT;
		    svar_T  *sv = find_typval_in_script(&di->di_tv, sid);

		    if (sv != NULL)
		    {
			// check the type and adjust to bool if needed
			where.wt_index = var_idx;
			where.wt_variable = TRUE;
			if (check_script_var_type(sv, tv, name, where) == FAIL)
			    goto failed;
			if (type == NULL)
			    type = sv->sv_type;
		    }
		}

		if ((flags & ASSIGN_FOR_LOOP) == 0
				     && var_check_permission(di, name) == FAIL)
		    goto failed;
	    }
	    else
	    {
		// can only redefine once
		di->di_flags &= ~DI_FLAGS_RELOAD;

		// A Vim9 script-local variable is also present in sn_all_vars
		// and sn_var_vals.  It may set "type" from "tv".
		if (var_in_vim9script || var_in_autoload)
		    update_vim9_script_var(FALSE, di,
			    var_in_autoload ? name : di->di_key, flags,
			    tv, &type, (flags & ASSIGN_NO_MEMBER_TYPE) == 0);
	    }

	    // existing variable, need to clear the value

	    // Handle setting internal di: variables separately where needed to
	    // prevent changing the type.
	    if (ht == &vimvarht)
	    {
		if (di->di_tv.v_type == VAR_STRING)
		{
		    VIM_CLEAR(di->di_tv.vval.v_string);
		    if (copy || tv->v_type != VAR_STRING)
		    {
			char_u *val = tv_get_string(tv);

			// Careful: when assigning to v:errmsg and
			// tv_get_string() causes an error message the variable
			// will already be set.
			if (di->di_tv.vval.v_string == NULL)
			    di->di_tv.vval.v_string = vim_strsave(val);
		    }
		    else
		    {
			// Take over the string to avoid an extra alloc/free.
			di->di_tv.vval.v_string = tv->vval.v_string;
			tv->vval.v_string = NULL;
		    }
		    goto failed;
		}
		else if (di->di_tv.v_type == VAR_NUMBER)
		{
		    di->di_tv.vval.v_number = tv_get_number(tv);
		    if (STRCMP(varname, "searchforward") == 0)
			set_search_direction(di->di_tv.vval.v_number
								  ? '/' : '?');
#ifdef FEAT_SEARCH_EXTRA
		    else if (STRCMP(varname, "hlsearch") == 0)
		    {
			no_hlsearch = !di->di_tv.vval.v_number;
			redraw_all_later(SOME_VALID);
		    }
#endif
		    goto failed;
		}
		else if (di->di_tv.v_type != tv->v_type)
		{
		    semsg(_(e_setting_str_to_value_with_wrong_type), name);
		    goto failed;
		}
	    }

	    clear_tv(&di->di_tv);
	}
	else
	{
	    // Item not found, check if a function already exists.
	    if (is_script_local && (flags & (ASSIGN_NO_DECL | ASSIGN_DECL)) == 0
		   && lookup_scriptitem(name, STRLEN(name), FALSE, NULL) == OK)
	    {
		semsg(_(e_redefining_script_item_str), name);
		goto failed;
	    }

	    // add a new variable
	    if (var_in_vim9script && (flags & ASSIGN_NO_DECL))
	    {
		semsg(_(e_unknown_variable_str), name);
		goto failed;
	    }

	    // Can't add "v:" or "a:" variable.
	    if (ht == &vimvarht || ht == get_funccal_args_ht())
	    {
		semsg(_(e_illegal_variable_name_str), name);
		goto failed;
	    }

	    // Make sure the variable name is valid.  In Vim9 script an
	    // autoload variable must be prefixed with "g:" unless in an
	    // autoload script.
	    if (!valid_varname(varname, -1, !vim9script
			    || STRNCMP(name, "g:", 2) == 0 || var_in_autoload))
		goto failed;

	    di = alloc(sizeof(dictitem_T) + STRLEN(varname));
	    if (di == NULL)
		goto failed;
	    STRCPY(di->di_key, varname);
	    if (hash_add(ht, DI2HIKEY(di)) == FAIL)
	    {
		vim_free(di);
		goto failed;
	    }
	    di->di_flags = DI_FLAGS_ALLOC;
	    if (flags & (ASSIGN_CONST | ASSIGN_FINAL))
		di->di_flags |= DI_FLAGS_LOCK;

	    // A Vim9 script-local variable is also added to sn_all_vars and
	    // sn_var_vals. It may set "type" from "tv".
	    if (var_in_vim9script || var_in_autoload)
		update_vim9_script_var(TRUE, di,
			var_in_autoload ? name : di->di_key, flags,
			      tv, &type, (flags & ASSIGN_NO_MEMBER_TYPE) == 0);
	}

	dest_tv = &di->di_tv;
    }

    if (copy || tv->v_type == VAR_NUMBER || tv->v_type == VAR_FLOAT)
	copy_tv(tv, dest_tv);
    else
    {
	*dest_tv = *tv;
	dest_tv->v_lock = 0;
	init_tv(tv);
    }
    free_tv_arg = FALSE;

    if (vim9script && type != NULL)
	set_tv_type(dest_tv, type);

    // ":const var = value" locks the value
    // ":final var = value" locks "var"
    if (flags & ASSIGN_CONST)
	// Like :lockvar! name: lock the value and what it contains, but only
	// if the reference count is up to one.  That locks only literal
	// values.
	item_lock(dest_tv, DICT_MAXNEST, TRUE, TRUE);

failed:
    vim_free(name_tofree);
    if (free_tv_arg)
	clear_tv(tv_arg);
}