ex_let(exarg_T *eap)
{
    char_u	*arg = eap->arg;
    char_u	*expr = NULL;
    typval_T	rettv;
    int		i;
    int		var_count = 0;
    int		semicolon = 0;
    char_u	op[4];
    char_u	*argend;
    int		first = TRUE;
    int		concat;
    int		has_assign;
    int		flags = 0;
    int		vim9script = in_vim9script();

    if (eap->cmdidx == CMD_final && !vim9script)
    {
	// In legacy Vim script ":final" is short for ":finally".
	ex_finally(eap);
	return;
    }
    if (eap->cmdidx == CMD_let && vim9script)
    {
	emsg(_(e_cannot_use_let_in_vim9_script));
	return;
    }

    if (eap->cmdidx == CMD_const)
	flags |= ASSIGN_CONST;
    else if (eap->cmdidx == CMD_final)
	flags |= ASSIGN_FINAL;

    // Vim9 assignment without ":let", ":const" or ":final"
    if (eap->arg == eap->cmd)
	flags |= ASSIGN_NO_DECL;

    argend = skip_var_list(arg, TRUE, &var_count, &semicolon, FALSE);
    if (argend == NULL)
	return;
    if (argend > arg && argend[-1] == '.')  // for var.='str'
	--argend;
    expr = skipwhite(argend);
    concat = expr[0] == '.'
	&& ((expr[1] == '=' && in_old_script(2))
		|| (expr[1] == '.' && expr[2] == '='));
    has_assign =  *expr == '=' || (vim_strchr((char_u *)"+-*/%", *expr) != NULL
							    && expr[1] == '=');
    if (!has_assign && !concat)
    {
	// ":let" without "=": list variables
	if (*arg == '[')
	    emsg(_(e_invalid_argument));
	else if (expr[0] == '.' && expr[1] == '=')
	    emsg(_(e_dot_equal_not_supported_with_script_version_two));
	else if (!ends_excmd2(eap->cmd, arg))
	{
	    if (vim9script)
	    {
		if (!ends_excmd2(eap->cmd, skipwhite(argend)))
		    semsg(_(e_trailing_characters_str), argend);
		else
		    // Vim9 declaration ":var name: type"
		    arg = vim9_declare_scriptvar(eap, arg);
	    }
	    else
	    {
		// ":let var1 var2" - list values
		arg = list_arg_vars(eap, arg, &first);
	    }
	}
	else if (!eap->skip)
	{
	    // ":let"
	    list_glob_vars(&first);
	    list_buf_vars(&first);
	    list_win_vars(&first);
	    list_tab_vars(&first);
	    list_script_vars(&first);
	    list_func_vars(&first);
	    list_vim_vars(&first);
	}
	set_nextcmd(eap, arg);
    }
    else if (expr[0] == '=' && expr[1] == '<' && expr[2] == '<')
    {
	list_T	*l;
	long	cur_lnum = SOURCING_LNUM;

	// HERE document
	l = heredoc_get(eap, expr + 3, FALSE);
	if (l != NULL)
	{
	    rettv_list_set(&rettv, l);
	    if (!eap->skip)
	    {
		// errors are for the assignment, not the end marker
		SOURCING_LNUM = cur_lnum;
		op[0] = '=';
		op[1] = NUL;
		(void)ex_let_vars(eap->arg, &rettv, FALSE, semicolon, var_count,
								flags, op);
	    }
	    clear_tv(&rettv);
	}
    }
    else
    {
	evalarg_T   evalarg;
	int	    len = 1;

	CLEAR_FIELD(rettv);
	i = FAIL;
	if (has_assign || concat)
	{
	    int cur_lnum;

	    op[0] = '=';
	    op[1] = NUL;
	    if (*expr != '=')
	    {
		if (vim9script && (flags & ASSIGN_NO_DECL) == 0)
		{
		    // +=, /=, etc. require an existing variable
		    semsg(_(e_cannot_use_operator_on_new_variable), eap->arg);
		    i = FAIL;
		}
		else if (vim_strchr((char_u *)"+-*/%.", *expr) != NULL)
		{
		    op[0] = *expr;   // +=, -=, *=, /=, %= or .=
		    ++len;
		    if (expr[0] == '.' && expr[1] == '.') // ..=
		    {
			++expr;
			++len;
		    }
		}
		expr += 2;
	    }
	    else
		++expr;

	    if (vim9script && !eap->skip && (!VIM_ISWHITE(*argend)
						   || !IS_WHITE_OR_NUL(*expr)))
	    {
		vim_strncpy(op, expr - len, len);
		semsg(_(e_white_space_required_before_and_after_str_at_str),
								   op, argend);
		i = FAIL;
	    }

	    if (eap->skip)
		++emsg_skip;
	    fill_evalarg_from_eap(&evalarg, eap, eap->skip);
	    expr = skipwhite_and_linebreak(expr, &evalarg);
	    cur_lnum = SOURCING_LNUM;
	    i = eval0(expr, &rettv, eap, &evalarg);
	    if (eap->skip)
		--emsg_skip;
	    clear_evalarg(&evalarg, eap);

	    // Restore the line number so that any type error is given for the
	    // declaration, not the expression.
	    SOURCING_LNUM = cur_lnum;
	}
	if (eap->skip)
	{
	    if (i != FAIL)
		clear_tv(&rettv);
	}
	else if (i != FAIL)
	{
	    (void)ex_let_vars(eap->arg, &rettv, FALSE, semicolon, var_count,
								    flags, op);
	    clear_tv(&rettv);
	}
    }
}