patch 8.2.3578: manipulating highlighting is complicated

Problem:    Manipulating highlighting is complicated.
Solution:   Add the hlget() and hlset() functions. (Yegappan Lakshmanan,
            closes #9039)
diff --git a/src/evalfunc.c b/src/evalfunc.c
index bbea3b1..ad9691e 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -1527,6 +1527,10 @@
 			ret_number,	    f_hlID},
     {"hlexists",	1, 1, FEARG_1,	    arg1_string,
 			ret_number_bool,    f_hlexists},
+    {"hlget",		0, 2, FEARG_1,	    arg2_string_bool,
+			ret_list_dict_any,  f_hlget},
+    {"hlset",		1, 1, FEARG_1,	    arg1_list_any,
+			ret_number_bool,    f_hlset},
     {"hostname",	0, 0, 0,	    NULL,
 			ret_string,	    f_hostname},
     {"iconv",		3, 3, FEARG_1,	    arg3_string,
diff --git a/src/highlight.c b/src/highlight.c
index 1226240..02790c6 100644
--- a/src/highlight.c
+++ b/src/highlight.c
@@ -4081,3 +4081,457 @@
 # endif
 }
 #endif
+
+#if defined(FEAT_EVAL) || defined(PROTO)
+/*
+ * Convert each of the highlight attribute bits (bold, standout, underline,
+ * etc.) set in 'hlattr' into a separate boolean item in a Dictionary with
+ * the attribute name as the key.
+ */
+    static dict_T *
+highlight_get_attr_dict(int hlattr)
+{
+    dict_T	*dict;
+    int		i;
+
+    dict = dict_alloc();
+    if (dict == NULL)
+	return NULL;
+
+    for (i = 0; hl_attr_table[i] != 0; ++i)
+    {
+	if (hlattr & hl_attr_table[i])
+	{
+	    dict_add_bool(dict, hl_name_table[i], VVAL_TRUE);
+	    hlattr &= ~hl_attr_table[i];	// don't want "inverse"
+	}
+    }
+
+    return dict;
+}
+
+/*
+ * Return the attributes of the highlight group at index 'hl_idx' as a
+ * Dictionary. If 'resolve_link' is TRUE, then resolves the highlight group
+ * links recursively.
+ */
+    static dict_T *
+highlight_get_info(int hl_idx, int resolve_link)
+{
+    dict_T	*dict;
+    hl_group_T	*sgp;
+    dict_T	*attr_dict;
+    int		hlgid;
+
+    dict = dict_alloc();
+    if (dict == NULL)
+	return dict;
+
+    sgp = &HL_TABLE()[hl_idx];
+    // highlight group id is 1-based
+    hlgid = hl_idx + 1;
+
+    if (dict_add_string(dict, "name", sgp->sg_name) == FAIL)
+	goto error;
+    if (dict_add_number(dict, "id", hlgid) == FAIL)
+	goto error;
+
+    if (sgp->sg_link && resolve_link)
+    {
+	// resolve the highlight group link recursively
+	while (sgp->sg_link)
+	{
+	    hlgid = sgp->sg_link;
+	    sgp = &HL_TABLE()[sgp->sg_link - 1];
+	}
+    }
+
+    if (sgp->sg_term != 0)
+    {
+	attr_dict = highlight_get_attr_dict(sgp->sg_term);
+	if (attr_dict != NULL)
+	    if (dict_add_dict(dict, "term", attr_dict) == FAIL)
+		goto error;
+    }
+    if (sgp->sg_start != NULL)
+	if (dict_add_string(dict, "start", sgp->sg_start) == FAIL)
+	    goto error;
+    if (sgp->sg_stop != NULL)
+	if (dict_add_string(dict, "stop", sgp->sg_stop) == FAIL)
+	    goto error;
+    if (sgp->sg_cterm != 0)
+    {
+	attr_dict = highlight_get_attr_dict(sgp->sg_cterm);
+	if (attr_dict != NULL)
+	    if (dict_add_dict(dict, "cterm", attr_dict) == FAIL)
+		goto error;
+    }
+    if (sgp->sg_cterm_fg != 0)
+	if (dict_add_string(dict, "ctermfg",
+		    highlight_color(hlgid, (char_u *)"fg", 'c')) == FAIL)
+	    goto error;
+    if (sgp->sg_cterm_bg != 0)
+	if (dict_add_string(dict, "ctermbg",
+			highlight_color(hlgid, (char_u *)"bg", 'c')) == FAIL)
+	    goto error;
+    if (sgp->sg_cterm_ul != 0)
+	if (dict_add_string(dict, "ctermul",
+			highlight_color(hlgid, (char_u *)"ul", 'c')) == FAIL)
+	    goto error;
+    if (sgp->sg_gui != 0)
+    {
+	attr_dict = highlight_get_attr_dict(sgp->sg_gui);
+	if (attr_dict != NULL)
+	    if (dict_add_dict(dict, "gui", attr_dict) == FAIL)
+		goto error;
+    }
+    if (sgp->sg_gui_fg_name != NULL)
+	if (dict_add_string(dict, "guifg",
+			highlight_color(hlgid, (char_u *)"fg", 'g')) == FAIL)
+	    goto error;
+    if (sgp->sg_gui_bg_name != NULL)
+	if (dict_add_string(dict, "guibg",
+			highlight_color(hlgid, (char_u *)"bg", 'g')) == FAIL)
+	    goto error;
+    if (sgp->sg_gui_sp_name != NULL)
+	if (dict_add_string(dict, "guisp",
+			highlight_color(hlgid, (char_u *)"sp", 'g')) == FAIL)
+	    goto error;
+# ifdef FEAT_GUI
+    if (sgp->sg_font_name != NULL)
+	if (dict_add_string(dict, "font", sgp->sg_font_name) == FAIL)
+	    goto error;
+# endif
+    if (sgp->sg_link)
+    {
+	char_u	*link;
+
+	link = HL_TABLE()[sgp->sg_link - 1].sg_name;
+	if (link != NULL && dict_add_string(dict, "linksto", link) == FAIL)
+	    goto error;
+    }
+    if (dict_len(dict) == 2)
+	// If only 'name' is present, then the highlight group is cleared.
+	dict_add_bool(dict, "cleared", VVAL_TRUE);
+
+    return dict;
+
+error:
+    vim_free(dict);
+    return NULL;
+}
+
+/*
+ * "hlget([name])" function
+ * Return the attributes of a specific highlight group (if specified) or all
+ * the highlight groups.
+ */
+    void
+f_hlget(typval_T *argvars, typval_T *rettv)
+{
+    list_T	*list;
+    dict_T	*dict;
+    int		i;
+    char_u	*hlarg = NULL;
+    int		resolve_link = FALSE;
+
+    if (rettv_list_alloc(rettv) == FAIL)
+	return;
+
+    if (check_for_opt_string_arg(argvars, 0) == FAIL
+	    || (argvars[0].v_type != VAR_UNKNOWN
+		&& check_for_opt_bool_arg(argvars, 1) == FAIL))
+	return;
+
+    if (argvars[0].v_type != VAR_UNKNOWN)
+    {
+	// highlight group name supplied
+	hlarg = tv_get_string_chk(&argvars[0]);
+	if (hlarg == NULL)
+	    return;
+
+	if (argvars[1].v_type != VAR_UNKNOWN)
+	{
+	    int error = FALSE;
+
+	    resolve_link = tv_get_bool_chk(&argvars[1], &error);
+	    if (error)
+		return;
+	}
+    }
+
+    list = rettv->vval.v_list;
+    for (i = 0; i < highlight_ga.ga_len && !got_int; ++i)
+    {
+	if (hlarg == NULL || STRICMP(hlarg, HL_TABLE()[i].sg_name) == 0)
+	{
+	    dict = highlight_get_info(i, resolve_link);
+	    if (dict != NULL)
+		list_append_dict(list, dict);
+	}
+    }
+}
+
+/*
+ * Returns the string value at 'dict[key]'. Returns NULL, if 'key' is not in
+ * 'dict' or the value is not a string type. If the value is not a string type
+ * or is NULL, then 'error' is set to TRUE.
+ */
+    static char_u *
+hldict_get_string(dict_T *dict, char_u *key, int *error)
+{
+    dictitem_T	*di;
+
+    *error = FALSE;
+    di = dict_find(dict, key, -1);
+    if (di == NULL)
+	return NULL;
+
+    if (di->di_tv.v_type != VAR_STRING || di->di_tv.vval.v_string == NULL)
+    {
+	emsg(_(e_stringreq));
+	*error = TRUE;
+	return NULL;
+    }
+
+    return di->di_tv.vval.v_string;
+}
+
+/*
+ * Convert the highlight attribute Dictionary at 'dict[key]' into a string
+ * value in 'attr_str' of length 'len'. Returns FALSE if 'dict[key]' is not a
+ * Dictionary or is NULL.
+ */
+    static int
+hldict_attr_to_str(
+	dict_T	*dict,
+	char_u	*key,
+	char_u	*attr_str,
+	int	len)
+{
+    dictitem_T	*di;
+    dict_T	*attrdict;
+    int		i;
+
+    attr_str[0] = NUL;
+    di = dict_find(dict, key, -1);
+    if (di == NULL)
+	return TRUE;
+
+    if (di->di_tv.v_type != VAR_DICT || di->di_tv.vval.v_dict == NULL)
+    {
+	emsg(_(e_dictreq));
+	return FALSE;
+    }
+
+    attrdict = di->di_tv.vval.v_dict;
+
+    // If the attribute dict is empty, then return NONE to clear the attributes
+    if (dict_len(attrdict) == 0)
+    {
+	vim_strcat(attr_str, (char_u *)"NONE", len);
+	return TRUE;
+    }
+
+    for (i = 0; i < (int)ARRAY_LENGTH(hl_name_table); i++)
+    {
+	if (dict_get_bool(attrdict, (char_u *)hl_name_table[i],
+		    VVAL_FALSE) == VVAL_TRUE)
+	{
+	    if (attr_str[0] != NUL)
+		vim_strcat(attr_str, (char_u *)",", len);
+	    vim_strcat(attr_str, (char_u *)hl_name_table[i], len);
+	}
+    }
+
+    return TRUE;
+}
+
+/*
+ * Add or update a highlight group using 'dict' items. Returns TRUE if
+ * successfully updated the highlight group.
+ */
+    static int
+hlg_add_or_update(dict_T *dict)
+{
+    char_u	*name;
+    int		error;
+    char_u	term_attr[80];
+    char_u	cterm_attr[80];
+    char_u	gui_attr[80];
+    char_u	*start;
+    char_u	*stop;
+    char_u	*ctermfg;
+    char_u	*ctermbg;
+    char_u	*ctermul;
+    char_u	*guifg;
+    char_u	*guibg;
+    char_u	*guisp;
+# ifdef FEAT_GUI
+    char_u	*font;
+# endif
+
+    name = hldict_get_string(dict, (char_u *)"name", &error);
+    if (name == NULL || error)
+	return FALSE;
+
+    if (dict_find(dict, (char_u *)"linksto", -1) != NULL)
+    {
+	char_u	*linksto;
+
+	// link highlight groups
+	linksto = hldict_get_string(dict, (char_u *)"linksto", &error);
+	if (linksto == NULL || error)
+	    return FALSE;
+
+	vim_snprintf((char *)IObuff, IOSIZE, "link %s %s", name, linksto);
+	do_highlight(IObuff, FALSE, FALSE);
+
+	return TRUE;
+    }
+
+    if (dict_find(dict, (char_u *)"cleared", -1) != NULL)
+    {
+	varnumber_T	cleared;
+
+	// clear a highlight group
+	cleared = dict_get_bool(dict, (char_u *)"cleared", FALSE);
+	if (cleared == TRUE)
+	{
+	    vim_snprintf((char *)IObuff, IOSIZE, "clear %s", name);
+	    do_highlight(IObuff, FALSE, FALSE);
+	}
+
+	return TRUE;
+    }
+
+    start = hldict_get_string(dict, (char_u *)"start", &error);
+    if (error)
+	return FALSE;
+
+    stop = hldict_get_string(dict, (char_u *)"stop", &error);
+    if (error)
+	return FALSE;
+
+    if (!hldict_attr_to_str(dict, (char_u *)"term", term_attr,
+		sizeof(term_attr)))
+	return FALSE;
+
+    if (!hldict_attr_to_str(dict, (char_u *)"cterm", cterm_attr,
+		sizeof(cterm_attr)))
+	return FALSE;
+
+    ctermfg = hldict_get_string(dict, (char_u *)"ctermfg", &error);
+    if (error)
+	return FALSE;
+
+    ctermbg = hldict_get_string(dict, (char_u *)"ctermbg", &error);
+    if (error)
+	return FALSE;
+
+    ctermul = hldict_get_string(dict, (char_u *)"ctermul", &error);
+    if (error)
+	return FALSE;
+
+    if (!hldict_attr_to_str(dict, (char_u *)"gui", gui_attr,
+		sizeof(gui_attr)))
+	return FALSE;
+
+    guifg = hldict_get_string(dict, (char_u *)"guifg", &error);
+    if (error)
+	return FALSE;
+
+    guibg = hldict_get_string(dict, (char_u *)"guibg", &error);
+    if (error)
+	return FALSE;
+
+    guisp = hldict_get_string(dict, (char_u *)"guisp", &error);
+    if (error)
+	return FALSE;
+
+# ifdef FEAT_GUI
+    font = hldict_get_string(dict, (char_u *)"font", &error);
+    if (error)
+	return FALSE;
+# endif
+
+    // If none of the attributes are specified, then do nothing.
+    if (term_attr[0] == NUL && start == NULL && stop == NULL
+	    && cterm_attr[0] == NUL && ctermfg == NULL && ctermbg == NULL
+	    && ctermul == NULL && gui_attr[0] == NUL
+# ifdef FEAT_GUI
+	    && font == NULL
+# endif
+	    && guifg == NULL && guibg == NULL && guisp == NULL
+	    )
+	return TRUE;
+
+    vim_snprintf((char *)IObuff, IOSIZE,
+	    "%s %s%s %s%s %s%s %s%s %s%s %s%s %s%s %s%s %s%s %s%s %s%s %s%s",
+	    name,
+	    term_attr[0] != NUL ? "term=" : "",
+	    term_attr[0] != NUL ? term_attr : (char_u *)"",
+	    start != NULL ? "start=" : "",
+	    start != NULL ? start : (char_u *)"",
+	    stop != NULL ? "stop=" : "",
+	    stop != NULL ? stop : (char_u *)"",
+	    cterm_attr[0] != NUL ? "cterm=" : "",
+	    cterm_attr[0] != NUL ? cterm_attr : (char_u *)"",
+	    ctermfg != NULL ? "ctermfg=" : "",
+	    ctermfg != NULL ? ctermfg : (char_u *)"",
+	    ctermbg != NULL ? "ctermbg=" : "",
+	    ctermbg != NULL ? ctermbg : (char_u *)"",
+	    ctermul != NULL ? "ctermul=" : "",
+	    ctermul != NULL ? ctermul : (char_u *)"",
+	    gui_attr[0] != NUL ? "gui=" : "",
+	    gui_attr[0] != NUL ? gui_attr : (char_u *)"",
+# ifdef FEAT_GUI
+	    font != NULL ? "font=" : "",
+	    font != NULL ? font : (char_u *)"",
+# else
+	    "", "",
+# endif
+	    guifg != NULL ? "guifg=" : "",
+	    guifg != NULL ? guifg : (char_u *)"",
+	    guibg != NULL ? "guibg=" : "",
+	    guibg != NULL ? guibg : (char_u *)"",
+	    guisp != NULL ? "guisp=" : "",
+	    guisp != NULL ? guisp : (char_u *)""
+		);
+
+    do_highlight(IObuff, FALSE, FALSE);
+
+    return TRUE;
+}
+
+/*
+ * "hlset([{highlight_attr}])" function
+ * Add or modify highlight groups
+ */
+    void
+f_hlset(typval_T *argvars, typval_T *rettv)
+{
+    listitem_T	*li;
+    dict_T	*dict;
+
+    rettv->vval.v_number = -1;
+
+    if (check_for_list_arg(argvars, 0) == FAIL)
+	return;
+
+    FOR_ALL_LIST_ITEMS(argvars->vval.v_list, li)
+    {
+	if (li->li_tv.v_type != VAR_DICT)
+	{
+	    emsg(_(e_dictreq));
+	    return;
+	}
+
+	dict = li->li_tv.vval.v_dict;
+	if (!hlg_add_or_update(dict))
+	    return;
+    }
+
+    rettv->vval.v_number = 0;
+}
+#endif
diff --git a/src/proto/highlight.pro b/src/proto/highlight.pro
index 4c6b2f7..7e2631d 100644
--- a/src/proto/highlight.pro
+++ b/src/proto/highlight.pro
@@ -49,4 +49,6 @@
 char_u *get_highlight_name(expand_T *xp, int idx);
 char_u *get_highlight_name_ext(expand_T *xp, int idx, int skip_cleared);
 void free_highlight_fonts(void);
+void f_hlget(typval_T *argvars, typval_T *rettv);
+void f_hlset(typval_T *argvars, typval_T *rettv);
 /* vim: set ft=c : */
diff --git a/src/testdir/test_highlight.vim b/src/testdir/test_highlight.vim
index d3734b3..8a7d1d9 100644
--- a/src/testdir/test_highlight.vim
+++ b/src/testdir/test_highlight.vim
@@ -4,6 +4,7 @@
 source screendump.vim
 source check.vim
 source script_util.vim
+source vim9.vim
 
 func Test_highlight()
   " basic test if ":highlight" doesn't crash
@@ -970,4 +971,164 @@
   call assert_fails("echo v:colornames['x1']")
 endfunc
 
+" Test for the hlget() function
+func Test_hlget()
+  let lines =<< trim END
+    call assert_notequal([], filter(hlget(), 'v:val.name == "Visual"'))
+    call assert_equal([], hlget('SomeHLGroup'))
+    highlight MyHLGroup term=standout cterm=reverse ctermfg=10 ctermbg=Black
+    call assert_equal([{'id': hlID('MyHLGroup'), 'ctermfg': '10', 'name': 'MyHLGroup', 'term': {'standout': v:true}, 'ctermbg': '0', 'cterm': {'reverse': v:true}}], hlget('MyHLGroup'))
+    highlight clear MyHLGroup
+    call assert_equal(v:true, hlget('MyHLGroup')[0].cleared)
+    highlight link MyHLGroup IncSearch
+    call assert_equal('IncSearch', hlget('MyHLGroup')[0].linksto)
+    highlight clear MyHLGroup
+    call assert_equal([], hlget(test_null_string()))
+    call assert_equal([], hlget(""))
+  END
+  call CheckLegacyAndVim9Success(lines)
+
+  " Test for resolving highlight group links
+  let lines =<< trim END
+    highlight hlgA term=bold
+    VAR hlgAid = hlID('hlgA')
+    highlight link hlgB hlgA
+    VAR hlgBid = hlID('hlgB')
+    highlight link hlgC hlgB
+    VAR hlgCid = hlID('hlgC')
+    call assert_equal('hlgA', hlget('hlgB')[0].linksto)
+    call assert_equal('hlgB', hlget('hlgC')[0].linksto)
+    call assert_equal([{'id': hlgAid, 'name': 'hlgA',
+                      \ 'term': {'bold': v:true}}], hlget('hlgA'))
+    call assert_equal([{'id': hlgBid, 'name': 'hlgB',
+                      \ 'linksto': 'hlgA'}], hlget('hlgB'))
+    call assert_equal([{'id': hlgCid, 'name': 'hlgC',
+                      \ 'linksto': 'hlgB'}], hlget('hlgC'))
+    call assert_equal([{'id': hlgAid, 'name': 'hlgA',
+                      \ 'term': {'bold': v:true}}], hlget('hlgA', v:false))
+    call assert_equal([{'id': hlgBid, 'name': 'hlgB',
+                      \ 'linksto': 'hlgA'}], hlget('hlgB', 0))
+    call assert_equal([{'id': hlgCid, 'name': 'hlgC',
+                      \ 'linksto': 'hlgB'}], hlget('hlgC', v:false))
+    call assert_equal([{'id': hlgAid, 'name': 'hlgA',
+                      \ 'term': {'bold': v:true}}], hlget('hlgA', v:true))
+    call assert_equal([{'id': hlgBid, 'name': 'hlgB',
+                      \ 'term': {'bold': v:true}}], hlget('hlgB', 1))
+    call assert_equal([{'id': hlgCid, 'name': 'hlgC',
+                      \ 'term': {'bold': v:true}}], hlget('hlgC', v:true))
+  END
+  call CheckLegacyAndVim9Success(lines)
+
+  call assert_fails('call hlget([])', 'E1174:')
+  call assert_fails('call hlget("abc", "xyz")', 'E1212:')
+endfunc
+
+" Test for the hlset() function
+func Test_hlset()
+  let lines =<< trim END
+    call assert_equal(0, hlset(test_null_list()))
+    call assert_equal(0, hlset([]))
+    call assert_fails('call hlset(["Search"])', 'E715:')
+    call hlset(hlget())
+    call hlset([{'name': 'NewHLGroup', 'cterm': {'reverse': v:true}, 'ctermfg': '10'}])
+    call assert_equal({'reverse': v:true}, hlget('NewHLGroup')[0].cterm)
+    call hlset([{'name': 'NewHLGroup', 'cterm': {'bold': v:true}}])
+    call assert_equal({'bold': v:true}, hlget('NewHLGroup')[0].cterm)
+    call hlset([{'name': 'NewHLGroup', 'cleared': v:true}])
+    call assert_equal(v:true, hlget('NewHLGroup')[0].cleared)
+    call hlset([{'name': 'NewHLGroup', 'linksto': 'Search'}])
+    call assert_false(has_key(hlget('NewHLGroup')[0], 'cleared'))
+    call assert_equal('Search', hlget('NewHLGroup')[0].linksto)
+    call assert_fails("call hlset([{'name': [], 'ctermfg': '10'}])", 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'cleared': []}])",
+          \ 'E745:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'cterm': 'Blue'}])",
+          \ 'E715:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'ctermbg': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'ctermfg': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'ctermul': []}])",
+          \ 'E928:')
+    if has('gui')
+      call assert_fails("call hlset([{'name': 'NewHLGroup', 'font': []}])",
+            \ 'E928:')
+    endif
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'gui': 'Cyan'}])",
+          \ 'E715:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'guibg': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'guifg': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'guisp': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'linksto': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'start': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'stop': []}])",
+          \ 'E928:')
+    call assert_fails("call hlset([{'name': 'NewHLGroup', 'term': 'Cyan'}])",
+          \ 'E715:')
+    call assert_equal('Search', hlget('NewHLGroup')[0].linksto)
+    highlight clear NewHLGroup
+  END
+  call CheckLegacyAndVim9Success(lines)
+
+  " Test for clearing the 'term', 'cterm' and 'gui' attributes of a highlight
+  " group.
+  let lines =<< trim END
+    highlight myhlg1 term=bold cterm=italic gui=standout
+    VAR id = hlID('myhlg1')
+    call hlset([{'name': 'myhlg1', 'term': {}}])
+    call assert_equal([{'id': id, 'name': 'myhlg1',
+                \ 'cterm': {'italic': v:true}, 'gui': {'standout': v:true}}],
+                \ hlget('myhlg1'))
+    call hlset([{'name': 'myhlg1', 'cterm': {}}])
+    call assert_equal([{'id': id, 'name': 'myhlg1',
+                \ 'gui': {'standout': v:true}}], hlget('myhlg1'))
+    call hlset([{'name': 'myhlg1', 'gui': {}}])
+    call assert_equal([{'id': id, 'name': 'myhlg1', 'cleared': v:true}],
+                \ hlget('myhlg1'))
+    highlight clear myhlg1
+  END
+  call CheckLegacyAndVim9Success(lines)
+
+  " Test for setting all the 'term', 'cterm' and 'gui' attributes of a
+  " highlight group
+  let lines =<< trim END
+    VAR attr = {'bold': v:true, 'underline': v:true, 'undercurl': v:true,
+                \ 'strikethrough': v:true, 'reverse': v:true, 'italic': v:true,
+                \ 'standout': v:true, 'nocombine': v:true}
+    call hlset([{'name': 'myhlg2', 'term': attr, 'cterm': attr, 'gui': attr}])
+    VAR id2 = hlID('myhlg2')
+    VAR output =<< trim END
+      myhlg2         xxx term=bold,standout,underline,undercurl,italic,reverse,nocombine,strikethrough
+                         cterm=bold,standout,underline,undercurl,italic,reverse,nocombine,strikethrough
+                         gui=bold,standout,underline,undercurl,italic,reverse,nocombine,strikethrough
+    END
+    call assert_equal(output, execute('highlight myhlg2')->split("\n"))
+    call assert_equal([{'id': id2, 'name': 'myhlg2', 'gui': attr,
+                      \ 'term': attr, 'cterm': attr}], hlget('myhlg2'))
+  END
+  call CheckLegacyAndVim9Success(lines)
+
+  " Test for clearing some of the 'term', 'cterm' and 'gui' attributes of a
+  " highlight group
+  let lines =<< trim END
+    VAR attr = {'bold': v:false, 'underline': v:true, 'strikethrough': v:true}
+    call hlset([{'name': 'myhlg2', 'term': attr, 'cterm': attr, 'gui': attr}])
+    VAR id2 = hlID('myhlg2')
+    VAR output =<< trim END
+      myhlg2         xxx term=underline,strikethrough cterm=underline,strikethrough
+                         gui=underline,strikethrough
+    END
+    call assert_equal(output, execute('highlight myhlg2')->split("\n"))
+    LET attr = {'underline': v:true, 'strikethrough': v:true}
+    call assert_equal([{'id': id2, 'name': 'myhlg2', 'gui': attr,
+                      \ 'term': attr, 'cterm': attr}], hlget('myhlg2'))
+  END
+  call CheckLegacyAndVim9Success(lines)
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim
index 03db4e6..e2b91d4 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -1721,6 +1721,16 @@
   hlexists('')->assert_equal(0)
 enddef
 
+def Test_hlget()
+  CheckDefAndScriptFailure2(['hlget([])'], 'E1013: Argument 1: type mismatch, expected string but got list<unknown>', 'E1174: String required for argument 1')
+  hlget('')->assert_equal([])
+enddef
+
+def Test_hlset()
+  CheckDefAndScriptFailure2(['hlset("id")'], 'E1013: Argument 1: type mismatch, expected list<any> but got string', 'E1211: List required for argument 1')
+  hlset([])->assert_equal(0)
+enddef
+
 def Test_iconv()
   CheckDefAndScriptFailure2(['iconv(1, "from", "to")'], 'E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1')
   CheckDefAndScriptFailure2(['iconv("abc", 10, "to")'], 'E1013: Argument 2: type mismatch, expected string but got number', 'E1174: String required for argument 2')