compile_assignment(char_u *arg, exarg_T *eap, cmdidx_T cmdidx, cctx_T *cctx)
{
    char_u	*var_start;
    char_u	*p;
    char_u	*end = arg;
    char_u	*ret = NULL;
    int		var_count = 0;
    int		var_idx;
    int		semicolon = 0;
    int		did_generate_slice = FALSE;
    garray_T	*instr = &cctx->ctx_instr;
    char_u	*op;
    int		oplen = 0;
    int		heredoc = FALSE;
    int		incdec = FALSE;
    type_T	*rhs_type = &t_any;
    char_u	*sp;
    int		is_decl = is_decl_command(cmdidx);
    lhs_T	lhs;
    long	start_lnum = SOURCING_LNUM;

    // Skip over the "var" or "[var, var]" to get to any "=".
    p = skip_var_list(arg, TRUE, &var_count, &semicolon, TRUE);
    if (p == NULL)
	return *arg == '[' ? arg : NULL;

    lhs.lhs_name = NULL;

    sp = p;
    p = skipwhite(p);
    op = p;
    oplen = assignment_len(p, &heredoc);

    if (var_count > 0 && oplen == 0)
	// can be something like "[1, 2]->func()"
	return arg;

    if (oplen > 0 && (!VIM_ISWHITE(*sp) || !IS_WHITE_OR_NUL(op[oplen])))
    {
	error_white_both(op, oplen);
	return NULL;
    }
    if (eap->cmdidx == CMD_increment || eap->cmdidx == CMD_decrement)
    {
	if (VIM_ISWHITE(eap->cmd[2]))
	{
	    semsg(_(e_no_white_space_allowed_after_str_str),
			 eap->cmdidx == CMD_increment ? "++" : "--", eap->cmd);
	    return NULL;
	}
	op = (char_u *)(eap->cmdidx == CMD_increment ? "+=" : "-=");
	oplen = 2;
	incdec = TRUE;
    }

    if (heredoc)
    {
	list_T	   *l;
	listitem_T *li;

	// [let] varname =<< [trim] {end}
	eap->getline = exarg_getline;
	eap->cookie = cctx;
	l = heredoc_get(eap, op + 3, FALSE);
	if (l == NULL)
	    return NULL;

	if (cctx->ctx_skip != SKIP_YES)
	{
	    // Push each line and the create the list.
	    FOR_ALL_LIST_ITEMS(l, li)
	    {
		generate_PUSHS(cctx, &li->li_tv.vval.v_string);
		li->li_tv.vval.v_string = NULL;
	    }
	    generate_NEWLIST(cctx, l->lv_len);
	}
	list_free(l);
	p += STRLEN(p);
	end = p;
    }
    else if (var_count > 0)
    {
	char_u *wp;

	// for "[var, var] = expr" evaluate the expression here, loop over the
	// list of variables below.
	// A line break may follow the "=".

	wp = op + oplen;
	if (may_get_next_line_error(wp, &p, cctx) == FAIL)
	    return FAIL;
	if (compile_expr0(&p, cctx) == FAIL)
	    return NULL;
	end = p;

	if (cctx->ctx_skip != SKIP_YES)
	{
	    type_T	*stacktype;
	    int		needed_list_len;
	    int		did_check = FALSE;

	    stacktype = cctx->ctx_type_stack.ga_len == 0 ? &t_void
						  : get_type_on_stack(cctx, 0);
	    if (stacktype->tt_type == VAR_VOID)
	    {
		emsg(_(e_cannot_use_void_value));
		goto theend;
	    }
	    if (need_type(stacktype, &t_list_any, -1, 0, cctx,
							 FALSE, FALSE) == FAIL)
		goto theend;
	    // If a constant list was used we can check the length right here.
	    needed_list_len = semicolon ? var_count - 1 : var_count;
	    if (instr->ga_len > 0)
	    {
		isn_T	*isn = ((isn_T *)instr->ga_data) + instr->ga_len - 1;

		if (isn->isn_type == ISN_NEWLIST)
		{
		    did_check = TRUE;
		    if (semicolon ? isn->isn_arg.number < needed_list_len
			    : isn->isn_arg.number != needed_list_len)
		    {
			semsg(_(e_expected_nr_items_but_got_nr),
					 needed_list_len, isn->isn_arg.number);
			goto theend;
		    }
		}
	    }
	    if (!did_check)
		generate_CHECKLEN(cctx, needed_list_len, semicolon);
	    if (stacktype->tt_member != NULL)
		rhs_type = stacktype->tt_member;
	}
    }

    /*
     * Loop over variables in "[var, var] = expr".
     * For "var = expr" and "let var: type" this is done only once.
     */
    if (var_count > 0)
	var_start = skipwhite(arg + 1);  // skip over the "["
    else
	var_start = arg;
    for (var_idx = 0; var_idx == 0 || var_idx < var_count; var_idx++)
    {
	int	instr_count = -1;
	int	save_lnum;
	int	skip_store = FALSE;

	if (var_start[0] == '_' && !eval_isnamec(var_start[1]))
	{
	    // Ignore underscore in "[a, _, b] = list".
	    if (var_count > 0)
	    {
		var_start = skipwhite(var_start + 2);
		continue;
	    }
	    emsg(_(e_cannot_use_underscore_here));
	    goto theend;
	}
	vim_free(lhs.lhs_name);

	/*
	 * Figure out the LHS type and other properties.
	 */
	if (compile_assign_lhs(var_start, &lhs, cmdidx,
					is_decl, heredoc, oplen, cctx) == FAIL)
	    goto theend;
	if (heredoc)
	{
	    SOURCING_LNUM = start_lnum;
	    if (lhs.lhs_has_type
		    && need_type(&t_list_string, lhs.lhs_type,
					    -1, 0, cctx, FALSE, FALSE) == FAIL)
		goto theend;
	}
	else
	{
	    if (cctx->ctx_skip == SKIP_YES)
	    {
		if (oplen > 0 && var_count == 0)
		{
		    // skip over the "=" and the expression
		    p = skipwhite(op + oplen);
		    (void)compile_expr0(&p, cctx);
		}
	    }
	    else if (oplen > 0)
	    {
		int	is_const = FALSE;
		char_u	*wp;

		// for "+=", "*=", "..=" etc. first load the current value
		if (*op != '='
			&& compile_load_lhs_with_index(&lhs, var_start,
								 cctx) == FAIL)
		    goto theend;

		// For "var = expr" evaluate the expression.
		if (var_count == 0)
		{
		    int	r;

		    // Compile the expression.
		    instr_count = instr->ga_len;
		    if (incdec)
		    {
			r = generate_PUSHNR(cctx, 1);
		    }
		    else
		    {
			// Temporarily hide the new local variable here, it is
			// not available to this expression.
			if (lhs.lhs_new_local)
			    --cctx->ctx_locals.ga_len;
			wp = op + oplen;
			if (may_get_next_line_error(wp, &p, cctx) == FAIL)
			{
			    if (lhs.lhs_new_local)
				++cctx->ctx_locals.ga_len;
			    goto theend;
			}
			r = compile_expr0_ext(&p, cctx, &is_const);
			if (lhs.lhs_new_local)
			    ++cctx->ctx_locals.ga_len;
			if (r == FAIL)
			    goto theend;
		    }
		}
		else if (semicolon && var_idx == var_count - 1)
		{
		    // For "[var; var] = expr" get the rest of the list
		    did_generate_slice = TRUE;
		    if (generate_SLICE(cctx, var_count - 1) == FAIL)
			goto theend;
		}
		else
		{
		    // For "[var, var] = expr" get the "var_idx" item from the
		    // list.
		    if (generate_GETITEM(cctx, var_idx, *op != '=') == FAIL)
			goto theend;
		}

		rhs_type = cctx->ctx_type_stack.ga_len == 0 ? &t_void
						  : get_type_on_stack(cctx, 0);
		if (lhs.lhs_lvar != NULL && (is_decl || !lhs.lhs_has_type))
		{
		    if ((rhs_type->tt_type == VAR_FUNC
				|| rhs_type->tt_type == VAR_PARTIAL)
			    && !lhs.lhs_has_index
			    && var_wrong_func_name(lhs.lhs_name, TRUE))
			goto theend;

		    if (lhs.lhs_new_local && !lhs.lhs_has_type)
		    {
			if (rhs_type->tt_type == VAR_VOID)
			{
			    emsg(_(e_cannot_use_void_value));
			    goto theend;
			}
			else
			{
			    // An empty list or dict has a &t_unknown member,
			    // for a variable that implies &t_any.
			    if (rhs_type == &t_list_empty)
				lhs.lhs_lvar->lv_type = &t_list_any;
			    else if (rhs_type == &t_dict_empty)
				lhs.lhs_lvar->lv_type = &t_dict_any;
			    else if (rhs_type == &t_unknown)
				lhs.lhs_lvar->lv_type = &t_any;
			    else
				lhs.lhs_lvar->lv_type = rhs_type;
			}
		    }
		    else if (*op == '=')
		    {
			type_T *use_type = lhs.lhs_lvar->lv_type;
			where_T where = WHERE_INIT;

			// Without operator check type here, otherwise below.
			// Use the line number of the assignment.
			SOURCING_LNUM = start_lnum;
			where.wt_index = var_count > 0 ? var_idx + 1 : 0;
			where.wt_variable = var_count > 0;
			// If assigning to a list or dict member, use the
			// member type.  Not for "list[:] =".
			if (lhs.lhs_has_index
				&& !has_list_index(var_start + lhs.lhs_varlen,
									 cctx))
			    use_type = lhs.lhs_member_type;
			if (need_type_where(rhs_type, use_type, -1, where,
				    cctx, FALSE, is_const) == FAIL)
			    goto theend;
		    }
		}
		else
		{
		    type_T *lhs_type = lhs.lhs_member_type;

		    // Special case: assigning to @# can use a number or a
		    // string.
		    // Also: can assign a number to a float.
		    if ((lhs_type == &t_number_or_string
				|| lhs_type == &t_float)
			    && rhs_type->tt_type == VAR_NUMBER)
			lhs_type = &t_number;
		    if (*p != '=' && need_type(rhs_type, lhs_type,
					    -1, 0, cctx, FALSE, FALSE) == FAIL)
		    goto theend;
		}
	    }
	    else if (cmdidx == CMD_final)
	    {
		emsg(_(e_final_requires_a_value));
		goto theend;
	    }
	    else if (cmdidx == CMD_const)
	    {
		emsg(_(e_const_requires_a_value));
		goto theend;
	    }
	    else if (!lhs.lhs_has_type || lhs.lhs_dest == dest_option
					   || lhs.lhs_dest == dest_func_option)
	    {
		emsg(_(e_type_or_initialization_required));
		goto theend;
	    }
	    else
	    {
		// variables are always initialized
		if (GA_GROW_FAILS(instr, 1))
		    goto theend;
		switch (lhs.lhs_member_type->tt_type)
		{
		    case VAR_BOOL:
			generate_PUSHBOOL(cctx, VVAL_FALSE);
			break;
		    case VAR_FLOAT:
#ifdef FEAT_FLOAT
			generate_PUSHF(cctx, 0.0);
#endif
			break;
		    case VAR_STRING:
			generate_PUSHS(cctx, NULL);
			break;
		    case VAR_BLOB:
			generate_PUSHBLOB(cctx, blob_alloc());
			break;
		    case VAR_FUNC:
			generate_PUSHFUNC(cctx, NULL, &t_func_void);
			break;
		    case VAR_LIST:
			generate_NEWLIST(cctx, 0);
			break;
		    case VAR_DICT:
			generate_NEWDICT(cctx, 0);
			break;
		    case VAR_JOB:
			generate_PUSHJOB(cctx, NULL);
			break;
		    case VAR_CHANNEL:
			generate_PUSHCHANNEL(cctx, NULL);
			break;
		    case VAR_NUMBER:
		    case VAR_UNKNOWN:
		    case VAR_ANY:
		    case VAR_PARTIAL:
		    case VAR_VOID:
		    case VAR_INSTR:
		    case VAR_SPECIAL:  // cannot happen
			// This is skipped for local variables, they are
			// always initialized to zero.
			if (lhs.lhs_dest == dest_local)
			    skip_store = TRUE;
			else
			    generate_PUSHNR(cctx, 0);
			break;
		}
	    }
	    if (var_count == 0)
		end = p;
	}

	// no need to parse more when skipping
	if (cctx->ctx_skip == SKIP_YES)
	    break;

	if (oplen > 0 && *op != '=')
	{
	    type_T	    *expected;
	    type_T	    *stacktype = NULL;

	    if (*op == '.')
	    {
		if (may_generate_2STRING(-1, FALSE, cctx) == FAIL)
		    goto theend;
	    }
	    else
	    {
		expected = lhs.lhs_member_type;
		stacktype = get_type_on_stack(cctx, 0);
		if (
#ifdef FEAT_FLOAT
		    // If variable is float operation with number is OK.
		    !(expected == &t_float && (stacktype == &t_number
			    || stacktype == &t_number_bool)) &&
#endif
		    need_type(stacktype, expected, -1, 0, cctx,
							 FALSE, FALSE) == FAIL)
		    goto theend;
	    }

	    if (*op == '.')
	    {
		if (generate_instr_drop(cctx, ISN_CONCAT, 1) == NULL)
		    goto theend;
	    }
	    else if (*op == '+')
	    {
		if (generate_add_instr(cctx,
			    operator_type(lhs.lhs_member_type, stacktype),
				       lhs.lhs_member_type, stacktype,
							  EXPR_APPEND) == FAIL)
		    goto theend;
	    }
	    else if (generate_two_op(cctx, op) == FAIL)
		goto theend;
	}

	// Use the line number of the assignment for store instruction.
	save_lnum = cctx->ctx_lnum;
	cctx->ctx_lnum = start_lnum - 1;

	if (lhs.lhs_has_index)
	{
	    // Use the info in "lhs" to store the value at the index in the
	    // list or dict.
	    if (compile_assign_unlet(var_start, &lhs, TRUE, rhs_type, cctx)
								       == FAIL)
	    {
		cctx->ctx_lnum = save_lnum;
		goto theend;
	    }
	}
	else
	{
	    if (is_decl && cmdidx == CMD_const && (lhs.lhs_dest == dest_script
						|| lhs.lhs_dest == dest_global
						|| lhs.lhs_dest == dest_local))
		// ":const var": lock the value, but not referenced variables
		generate_LOCKCONST(cctx);

	    if ((lhs.lhs_type->tt_type == VAR_DICT
					  || lhs.lhs_type->tt_type == VAR_LIST)
		    && lhs.lhs_type->tt_member != NULL
		    && lhs.lhs_type->tt_member != &t_unknown)
		// Set the type in the list or dict, so that it can be checked,
		// also in legacy script.
		generate_SETTYPE(cctx, lhs.lhs_type);

	    if (!skip_store && generate_store_lhs(cctx, &lhs,
						 instr_count, is_decl) == FAIL)
	    {
		cctx->ctx_lnum = save_lnum;
		goto theend;
	    }
	}
	cctx->ctx_lnum = save_lnum;

	if (var_idx + 1 < var_count)
	    var_start = skipwhite(lhs.lhs_end + 1);
    }

    // For "[var, var] = expr" drop the "expr" value.
    // Also for "[var, var; _] = expr".
    if (var_count > 0 && (!semicolon || !did_generate_slice))
    {
	if (generate_instr_drop(cctx, ISN_DROP, 1) == NULL)
	    goto theend;
    }

    ret = skipwhite(end);

theend:
    vim_free(lhs.lhs_name);
    return ret;
}