Commit: patch 9.2.0972: extend() family does not accept a blob

Christian Brabandt <[email protected]>
Newsgroups gmane.editors.vim.devel
Message-ID <[email protected]>
patch 9.2.0972: extend() family does not accept a blob

Commit: https://github.com/vim/vim/commit/bad0490bfb340bd4c59399903323c02c4bfbc5e3
Author: Bakudankun <[email protected]>
Date:   Tue Aug 18 20:22:38 2026 +0000

    patch 9.2.0972: extend() family does not accept a blob
    
    Problem:  extend() family does not accept a blob type
    Solution: Implement missing blob functionality
              (Bakudankun).
    
    closes: #20958
    
    Co-authored-by: Hirohito Higashi <[email protected]>
    Signed-off-by: Bakudankun <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 11934bcfc..737cac1fe 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -187,12 +187,12 @@ expand({expr} [, {nosuf} [, {list}]])
 expandcmd({string} [, {options}])
 				String	expand {string} like with `:edit`
 extend({expr1}, {expr2} [, {expr3}])
-				List/Dict
+				List/Dict/Blob
 					insert items of {expr2} into {expr1}
 extendnew({expr1}, {expr2} [, {expr3}])
-				List/Dict
-					like |extend()| but creates a new List
-					or Dictionary
+				List/Dict/Blob
+					like |extend()| but creates a new
+					List, Dictionary or Blob
 feedkeys({string} [, {mode}])	none	add key sequence to typeahead buffer
 filecopy({from}, {to})		Number	|TRUE| if copying file {from} to {to}
 					worked
@@ -633,8 +633,8 @@ simplify({filename})		String	simplify filename as much as possible
 sin({expr})			Float	sine of {expr}
 sinh({expr})			Float	hyperbolic sine of {expr}
 slice({expr}, {start} [, {end}])
-				String/List/Blob
-					slice of a String, List or Blob
+				String/List/Blob/Tuple
+					slice of a String, List, Blob or Tuple
 sort({list} [, {how} [, {dict}]])
 				List	sort {list}, compare with {how}
 sound_clear()			none	stop playing all sounds
@@ -3130,24 +3130,27 @@ expandcmd({string} [, {options}])			*expandcmd()*
 
 
 extend({expr1}, {expr2} [, {expr3}])			*extend()*
-		{expr1} and {expr2} must be both |Lists| or both
-		|Dictionaries|.
+		{expr1} and {expr2} must be both |Lists|, |Dictionaries| or
+		|Blobs|.
 
-		If they are |Lists|: Append {expr2} to {expr1}.
+		If they are |Lists| or |Blobs|: Append {expr2} to {expr1}.
 		If {expr3} is given insert the items of {expr2} before the
-		item with index {expr3} in {expr1}.  When {expr3} is zero
+		item with index/byte {expr3} in {expr1}.  When {expr3} is zero
 		insert before the first item.  When {expr3} is equal to
 		len({expr1}) then {expr2} is appended.
 		Examples: >
 			:echo sort(extend(mylist, [7, 5]))
 			:call extend(mylist, [2, 3], 1)
-<		When {expr1} is the same List as {expr2} then the number of
-		items copied is equal to the original length of the List.
+<		When {expr1} is the same List or Blob as {expr2} then the
+		number of items copied is equal to the original length of the
+		List or the Blob.
 		E.g., when {expr3} is 1 you get N new copies of the first item
 		(where N is the original length of the List).
-		Use |add()| to concatenate one item to a list.  To concatenate
-		two lists into a new list use the + operator: >
+		Use |add()| to concatenate one item to a list or a blob.
+		To concatenate two lists or blobs into a new one use the +
+		operator: >
 			:let newlist = [1, 2, 3] + [4, 5]
+			:let newblob = 0z1122 + 0z3344
 <
 		If they are |Dictionaries|:
 		Add all entries from {expr2} to {expr1}.
@@ -3160,7 +3163,7 @@ extend({expr1}, {expr2} [, {expr3}])			*extend()*
 
 		{expr1} is changed when {expr2} is not empty.  If necessary
 		make a copy of {expr1} first or use |extendnew()| to return a
-		new List/Dictionary.
+		new List/Dictionary/Blob.
 		{expr2} remains unchanged.
 		When {expr1} is locked and {expr2} is not empty the operation
 		fails.
@@ -3169,17 +3172,17 @@ extend({expr1}, {expr2} [, {expr3}])			*extend()*
 		Can also be used as a |method|: >
 			mylist->extend(otherlist)
 <
-		Return type: list<{type}> or dict<{type}> depending on {expr1}
-		and {expr2}, in case of error: |Number|
+		Return type: list<{type}>, dict<{type}> or blob depending on
+		{expr1} and {expr2}, in case of error: |Number|
 
 
 extendnew({expr1}, {expr2} [, {expr3}])			*extendnew()*
 		Like |extend()| but instead of adding items to {expr1} a new
-		List or Dictionary is created and returned.  {expr1} remains
-		unchanged.
+		List, Dictionary or Blob is created and returned.  {expr1}
+		remains unchanged.
 
-		Return type: list<{type}> or dict<{type}> depending on {expr1}
-		and {expr2}, in case of error: |Number|
+		Return type: list<{type}>, dict<{type}> or blob depending on
+		{expr1} and {expr2}, in case of error: |Number|
 
 
 feedkeys({string} [, {mode}])				*feedkeys()*
@@ -10731,7 +10734,7 @@ slice({expr}, {start} [, {end}])			*slice()*
 		Can also be used as a |method|: >
 			GetList()->slice(offset)
 <
-		Return type: list<{type}> or tuple<{type}>
+		Return type: |String|, list<{type}>, blob or tuple<{type}>
 
 
 sort({list} [, {how} [, {dict}]])			*sort()* *E702*
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index cedb60d28..c8f3aa6c2 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1,4 +1,4 @@
-*eval.txt*	For Vim version 9.2.  Last change: 2026 Aug 17
+*eval.txt*	For Vim version 9.2.  Last change: 2026 Aug 18
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -993,8 +993,10 @@ Two blobs can be concatenated with the "+" operator: >
 	:let longblob = myblob + 0z4455
 	:let longblob = 0z4455 + myblob
 <
-A blob can be concatenated with another one in-place using |:let+=|: >
+A blob can be concatenated with another one in-place using |:let+=| or
+|extend()|: >
 	:let myblob += 0z6677
+	:call extend(myblob, 0z6677)
 <
 See |blob-modification| below for more about changing a blob in-place.
 
@@ -1039,9 +1041,11 @@ To change part of a blob you can specify the first and last byte to be
 modified.  The value must have the same number of bytes in the range: >
 	:let blob[3:5] = 0z334455
 
-To add items to a Blob in-place, you can use |:let+=| (|blob-concatenation|): >
+To add items to a Blob in-place, you can use |:let+=| or |extend()|
+(|blob-concatenation|): >
 	:let blobA = 0z1122
 	:let blobA += 0z3344
+	:call extend(blobA, 0z5566)
 <
 When two variables refer to the same Blob, changing one Blob in-place will
 cause the referenced Blob to be changed in-place: >
diff --git a/runtime/doc/tags b/runtime/doc/tags
index a8ff635fe..ca5828a1e 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -4790,6 +4790,7 @@ E1576	tagsrch.txt	/*E1576*
 E1577	options.txt	/*E1577*
 E1578	spell.txt	/*E1578*
 E158	sign.txt	/*E158*
+E1581	vim9.txt	/*E1581*
 E159	sign.txt	/*E159*
 E16	cmdline.txt	/*E16*
 E160	sign.txt	/*E160*
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index cb816a0cb..c1ecf81b2 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1,4 +1,4 @@
-*usr_41.txt*	For Vim version 9.2.  Last change: 2026 Jun 09
+*usr_41.txt*	For Vim version 9.2.  Last change: 2026 Aug 18
 
 
 		     VIM USER MANUAL	by Bram Moolenaar
@@ -921,11 +921,28 @@ Floating point computation:				*float-functions*
 	isnan()			check for not a number
 
 Blob manipulation:					*blob-functions*
+	get()			get a byte without error for wrong index
+	len()			number of bytes in a Blob
+	empty()			check if Blob is empty
+	insert()		insert a byte somewhere in a Blob
+	add()			append a byte to a Blob
+	extend()		append a Blob to a Blob
+	extendnew()		make a new Blob and append bytes
+	remove()		remove one or more bytes from a Blob
+	copy()			make a copy of a Blob
+	filter()		remove selected bytes from a Blob
+	map()			change each byte of a Blob
+	mapnew()		make a new Blob with changed bytes
+	foreach()		apply function to Blob bytes
+	reduce()		reduce a Blob to a value
+	slice()			take a slice of a Blob
 	blob2list()		get a list of numbers from a blob
 	list2blob()		get a blob from a list of numbers
 	reverse()		reverse the order of numbers in a blob
+	string()		String representation of a Blob
 	index()			index of a value in a Blob
 	indexof()		index in a Blob where an expression is true
+	repeat()		repeat a Blob multiple times
 	items()			get List of Blob index-value pairs
 
 Other computation:					*bitwise-function*
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index f38fe1693..e2675c5f2 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt*	For Vim version 9.2.  Last change: 2026 Aug 16
+*version9.txt*	For Vim version 9.2.  Last change: 2026 Aug 18
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -52700,6 +52700,9 @@ Changed ~
 - |serverlist()| can return a list of all available server names.
 - Improve visual appearance when the terminal does not support colors.
 - Improve diff highlighting for terminals with 256 or less colors.
+- |listener_add()| accepts a Dictionary of options and can include the
+  resulting text of a change |listener-text|.
+- |extend()| and |extendnew()| also accept |Blobs|.
 
 
 							*added-9.3*
diff --git a/runtime/doc/vim9.txt b/runtime/doc/vim9.txt
index e69b62acd..082b9ba85 100644
--- a/runtime/doc/vim9.txt
+++ b/runtime/doc/vim9.txt
@@ -1,4 +1,4 @@
-*vim9.txt*	For Vim version 9.2.  Last change: 2026 Aug 16
+*vim9.txt*	For Vim version 9.2.  Last change: 2026 Aug 18
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -495,7 +495,7 @@ And with autocommands: >
 Although using a :def function probably works better.
 
 				*E1022* *E1103* *E1130* *E1131* *E1133*
-				*E1134*
+				*E1134* *E1581*
 Declaring a variable with a type but without an initializer will initialize to
 false (for bool), empty (for string, list, dict, etc.) or zero (for number,
 any, etc.).  This matters especially when using the "any" type, the value will
diff --git a/src/blob.c b/src/blob.c
index ad2b02bbb..3f7417409 100644
--- a/src/blob.c
+++ b/src/blob.c
@@ -57,34 +57,37 @@ rettv_blob_set(typval_T *rettv, blob_T *b)
 	++b->bv_refcount;
 }
 
-    int
-blob_copy(blob_T *from, typval_T *to)
+/*
+ * Create a copy of a blob.
+ * The refcount of the new blob is set to 1.
+ * Returns NULL when "orig" is NULL or out of memory.
+ */
+    blob_T *
+blob_copy(blob_T *orig)
 {
     int		len;
+    blob_T	*copy;
 
-    to->v_type = VAR_BLOB;
-    to->v_lock = 0;
-    if (from == NULL)
-    {
-	to->vval.v_blob = NULL;
-	return OK;
-    }
+    if (orig == NULL)
+	return NULL;
 
-    if (rettv_blob_alloc(to) == FAIL)
-	return FAIL;
+    copy = blob_alloc();
+    if (copy == NULL)
+	return NULL;
+    ++copy->bv_refcount;
 
-    len = from->bv_ga.ga_len;
+    len = orig->bv_ga.ga_len;
     if (len > 0)
     {
-	to->vval.v_blob->bv_ga.ga_data =
-	    vim_memsave(from->bv_ga.ga_data, len);
-	if (to->vval.v_blob->bv_ga.ga_data == NULL)
+	copy->bv_ga.ga_data =
+	    vim_memsave(orig->bv_ga.ga_data, len);
+	if (copy->bv_ga.ga_data == NULL)
 	    len = 0;
     }
-    to->vval.v_blob->bv_ga.ga_len = len;
-    to->vval.v_blob->bv_ga.ga_maxlen = len;
+    copy->bv_ga.ga_len = len;
+    copy->bv_ga.ga_maxlen = len;
 
-    return OK;
+    return copy;
 }
 
     void
@@ -666,7 +669,9 @@ blob_filter_map(
     b_ret = b;
     if (filtermap == FILTERMAP_MAPNEW)
     {
-	if (blob_copy(b, rettv) == FAIL)
+	rettv->v_lock = 0;
+	rettv->vval.v_blob = blob_copy(b);
+	if (rettv->vval.v_blob == NULL)
 	    return;
 	b_ret = rettv->vval.v_blob;
     }
@@ -774,6 +779,121 @@ blob_insert_func(typval_T *argvars, typval_T *rettv)
     copy_tv(&argvars[0], rettv);
 }
 
+/*
+ * Extend "b1" with "b2".  "b1" must not be NULL.
+ * If "bef" is equal to the length of b1 append at the end,
+ * otherwise insert before this index.
+ * Caller must check that "bef" is valid.
+ * Returns FAIL when out of memory.
+ */
+    static int
+blob_extend(blob_T *b1, blob_T *b2, long bef)
+{
+    int		len1, len2;
+    char_u	*p1;
+
+    // NULL blob is equivalent to an empty blob: nothing to do.
+    if (b2 == NULL || b2->bv_ga.ga_len == 0)
+	return OK;
+
+    len1 = b1->bv_ga.ga_len;
+    len2 = b2->bv_ga.ga_len;
+
+    if (ga_grow(&b1->bv_ga, len2) == FAIL)
+	return FAIL;
+
+    p1 = (char_u *)b1->bv_ga.ga_data;
+
+    if (b1 == b2)
+    {
+	// Inserting a blob into itself
+	mch_memmove(p1 + bef, p1, (size_t)len1);
+	if (bef < len1)
+	    memcpy(p1 + bef + len1, p1 + bef * 2, (size_t)(len1 - bef));
+    }
+    else
+    {
+	if (bef < len1)
+	    mch_memmove(p1 + bef + len2, p1 + bef, (size_t)(len1 - bef));
+
+	memcpy(p1 + bef, b2->bv_ga.ga_data, (size_t)len2);
+    }
+
+    b1->bv_ga.ga_len += len2;
+
+    return OK;
+}
+
+/*
+ * extend() a Blob. Append Blob argvars[1] to Blob argvars[0] before index
+ * argvars[3] and return the resulting blob in "rettv".  "is_new" is TRUE for
+ * extendnew().
+ */
+    void
+blob_extend_func(
+	typval_T	*argvars,
+	char_u		*arg_errmsg,
+	int		is_new,
+	typval_T	*rettv)
+{
+    blob_T	*b1, *b2;
+    long	before;
+    int		error = FALSE;
+
+    b1 = argvars[0].vval.v_blob;
+    if (b1 == NULL)
+    {
+	emsg(_(e_cannot_extend_null_blob));
+	return;
+    }
+    if (is_new || !value_check_lock(b1->bv_lock, arg_errmsg, TRUE))
+    {
+	if (is_new)
+	{
+	    b1 = blob_copy(b1);
+	    if (b1 == NULL)
+		return;
+	}
+
+	b2 = argvars[1].vval.v_blob;
+	if (b2 == NULL)
+	    goto theend;
+
+	if (argvars[2].v_type != VAR_UNKNOWN)
+	{
+	    before = (long)tv_get_number_chk(&argvars[2], &error);
+	    if (error)
+		goto cleanup;		// type error; errmsg already given
+	    if (before < 0)
+		before = b1->bv_ga.ga_len + before;
+	    if (before < 0 || before > b1->bv_ga.ga_len)
+	    {
+		semsg(_(e_blob_index_out_of_range_nr), before);
+		goto cleanup;
+	    }
+	}
+	else
+	    before = b1->bv_ga.ga_len;
+	if (blob_extend(b1, b2, before) == FAIL)
+	    goto cleanup;
+
+theend:
+	if (is_new)
+	{
+	    rettv->v_type = VAR_BLOB;
+	    rettv->v_lock = 0;
+	    rettv->vval.v_blob = b1;
+	}
+	else
+	    copy_tv(&argvars[0], rettv);
+	return;
+
+cleanup:
+	if (is_new)
+	    blob_unref(b1);
+    }
+}
+
 /*
  * Implementation of reduce() for Blob "argvars[0]" using the function "expr"
  * starting with the optional initial value "argvars[2]" and return the result
diff --git a/src/errors.h b/src/errors.h
index 350ffe704..b0e56608d 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3822,3 +3822,7 @@ EXTERN char e_completeopt_escape_cannot_be_used_with_nargs_underscore[]
 EXTERN char e_too_many_text_properties_on_a_single_line[]
 	INIT(= N_("E1580: Too many text properties on a single line"));
 #endif
+#ifdef FEAT_EVAL
+EXTERN char e_cannot_extend_null_blob[]
+	INIT(= N_("E1581: Cannot extend a null blob"));
+#endif
diff --git a/src/eval.c b/src/eval.c
index 759c4acb4..bb6f452b0 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -2890,12 +2890,9 @@ eval_for_line(
 		fi->fi_bi = 0;
 		if (tv.vval.v_blob != NULL)
 		{
-		    typval_T btv;
-
 		    // Make a copy, so that the iteration still works when the
 		    // blob is changed.
-		    blob_copy(tv.vval.v_blob, &btv);
-		    fi->fi_blob = btv.vval.v_blob;
+		    fi->fi_blob = blob_copy(tv.vval.v_blob);
 		}
 		clear_tv(&tv);
 	    }
@@ -7886,7 +7883,16 @@ item_copy(
 		ret = FAIL;
 	    break;
 	case VAR_BLOB:
-	    ret = blob_copy(from->vval.v_blob, to);
+	    to->v_type = VAR_BLOB;
+	    to->v_lock = 0;
+	    if (from->vval.v_blob == NULL)
+		to->vval.v_blob = NULL;
+	    else
+	    {
+		to->vval.v_blob = blob_copy(from->vval.v_blob);
+		if (to->vval.v_blob == NULL)
+		    ret = FAIL;
+	    }
 	    break;
 	case VAR_DICT:
 	    to->v_type = VAR_DICT;
diff --git a/src/evalfunc.c b/src/evalfunc.c
index bf8f00f7a..c2c2a6ab3 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -627,12 +627,16 @@ arg_string_or_blob(
 }
 
 /*
- * Check "type" is a list of 'any' or a dict of 'any'.
+ * Check "type" is a list of 'any', a tuple of 'any' or dict of 'any'.
  */
     static int
-arg_list_or_dict(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
+arg_list_or_tuple_or_dict(
+    type_T		*type,
+    type_T		*decl_type UNUSED,
+    argcontext_T	*context)
 {
     if (type->tt_type == VAR_LIST
+	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_DICT
 	    || type_any_or_unknown(type))
 	return OK;
@@ -641,31 +645,17 @@ arg_list_or_dict(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
 }
 
 /*
- * Check "type" is a list of 'any' or a dict of 'any'.  And modifiable.
+ * Check "type" is a list of 'any', a dict of 'any' or a blob.
  */
     static int
-arg_list_or_dict_mod(
+arg_list_or_dict_or_blob(
 	type_T	     *type,
-	type_T	     *decl_type,
+	type_T	     *decl_type UNUSED,
 	argcontext_T *context)
-{
-    if (arg_list_or_dict(type, decl_type, context) == FAIL)
-	return FAIL;
-    return arg_type_modifiable(type, context->arg_idx + 1);
-}
-
-/*
- * Check "type" is a list of 'any', a tuple of 'any' or dict of 'any'.
- */
-    static int
-arg_list_or_tuple_or_dict(
-    type_T		*type,
-    type_T		*decl_type UNUSED,
-    argcontext_T	*context)
 {
     if (type->tt_type == VAR_LIST
-	    || type->tt_type == VAR_TUPLE
 	    || type->tt_type == VAR_DICT
+	    || type->tt_type == VAR_BLOB
 	    || type_any_or_unknown(type))
 	return OK;
     arg_type_mismatch(&t_list_any, type, context->arg_idx + 1);
@@ -679,16 +669,12 @@ arg_list_or_tuple_or_dict(
     static int
 arg_list_or_dict_or_blob_mod(
 	type_T	     *type,
-	type_T	     *decl_type UNUSED,
+	type_T	     *decl_type,
 	argcontext_T *context)
 {
-    if (type->tt_type == VAR_LIST
-	    || type->tt_type == VAR_DICT
-	    || type->tt_type == VAR_BLOB
-	    || type_any_or_unknown(type))
-	return arg_type_modifiable(type, context->arg_idx + 1);
-    arg_type_mismatch(&t_list_any, type, context->arg_idx + 1);
-    return FAIL;
+    if (arg_list_or_dict_or_blob(type, decl_type, context) == FAIL)
+	return FAIL;
+    return arg_type_modifiable(type, context->arg_idx + 1);
 }
 
 /*
@@ -1107,7 +1093,7 @@ arg_extend3(type_T *type, type_T *decl_type, argcontext_T *context)
 {
     type_T *first_type = context->arg_types[context->arg_idx - 2].type_curr;
 
-    if (first_type->tt_type == VAR_LIST)
+    if (first_type->tt_type == VAR_LIST || first_type->tt_type == VAR_BLOB)
 	return arg_number(type, decl_type, context);
     if (first_type->tt_type == VAR_DICT)
 	return arg_string(type, decl_type, context);
@@ -1359,8 +1345,8 @@ static argcheck_T arg13_cursor[] = {arg_cursor1, arg_number, arg_number};
 static argcheck_T arg12_deepcopy[] = {arg_any, arg_bool};
 static argcheck_T arg12_execute[] = {arg_string_or_list_string, arg_string};
 static argcheck_T arg12_getchar[] = {arg_bool_or_nr, arg_dict_any};
-static argcheck_T arg23_extend[] = {arg_list_or_dict_mod, arg_same_as_prev, arg_extend3};
-static argcheck_T arg23_extendnew[] = {arg_list_or_dict, arg_same_struct_as_prev, arg_extend3};
+static argcheck_T arg23_extend[] = {arg_list_or_dict_or_blob_mod, arg_same_as_prev, arg_extend3};
+static argcheck_T arg23_extendnew[] = {arg_list_or_dict_or_blob, arg_same_struct_as_prev, arg_extend3};
 static argcheck_T arg23_get[] = {arg_get1, arg_string_or_nr, arg_any};
 static argcheck_T arg14_glob[] = {arg_string, arg_bool, arg_bool, arg_bool};
 static argcheck_T arg25_globpath[] = {arg_string, arg_string, arg_bool, arg_bool, arg_bool};
@@ -1665,6 +1651,8 @@ ret_extend(int argcount,
 		return &t_list_any;
 	    if (argtypes[0].type_curr->tt_type == VAR_DICT)
 		return &t_dict_any;
+	    if (argtypes[0].type_curr->tt_type == VAR_BLOB)
+		return &t_blob;
 	}
 	return argtypes[0].type_curr;
     }
diff --git a/src/list.c b/src/list.c
index 0ea2e13bd..24efe6bcf 100644
--- a/src/list.c
+++ b/src/list.c
@@ -3176,13 +3176,16 @@ extend(typval_T *argvars, typval_T *rettv, char_u *arg_errmsg, int is_new)
 	    type = argvars[0].vval.v_dict->dv_type;
 	dict_extend_func(argvars, type, func_name, arg_errmsg, is_new, rettv);
     }
+    else if (argvars[0].v_type == VAR_BLOB && argvars[1].v_type == VAR_BLOB)
+	blob_extend_func(argvars, arg_errmsg, is_new, rettv);
     else
-	semsg(_(e_argument_of_str_must_be_list_or_dictionary), func_name);
+	semsg(_(e_argument_of_str_must_be_list_dictionary_or_blob), func_name);
 }
 
 /*
  * "extend(list, list [, idx])" function
  * "extend(dict, dict [, action])" function
+ * "extend(blob, blob [, idx])" function
  */
     void
 f_extend(typval_T *argvars, typval_T *rettv)
@@ -3195,6 +3198,7 @@ f_extend(typval_T *argvars, typval_T *rettv)
 /*
  * "extendnew(list, list [, idx])" function
  * "extendnew(dict, dict [, action])" function
+ * "extendnew(blob, blob [, idx])" function
  */
     void
 f_extendnew(typval_T *argvars, typval_T *rettv)
diff --git a/src/po/vim.pot b/src/po/vim.pot
index 8f7f2e070..fbf6c473f 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim
"
 "Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-07-29 19:37+0000
"
+"POT-Creation-Date: 2026-08-18 20:22+0000
"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
 "Language-Team: LANGUAGE <[email protected]>
"
@@ -8902,6 +8902,9 @@ msgstr ""
 msgid "E1580: Too many text properties on a single line"
 msgstr ""
 
+msgid "E1581: Cannot extend a null blob"
+msgstr ""
+
 #. type of cmdline window or 0
 #. result of cmdline window or 0
 #. buffer of cmdline window or NULL
diff --git a/src/proto/blob.pro b/src/proto/blob.pro
index f24c268ae..cd32dcba7 100644
--- a/src/proto/blob.pro
+++ b/src/proto/blob.pro
@@ -2,7 +2,7 @@
 blob_T *blob_alloc(void);
 int rettv_blob_alloc(typval_T *rettv);
 void rettv_blob_set(typval_T *rettv, blob_T *b);
-int blob_copy(blob_T *from, typval_T *to);
+blob_T *blob_copy(blob_T *orig);
 void blob_free(blob_T *b);
 void blob_unref(blob_T *b);
 long blob_len(blob_T *b);
@@ -23,6 +23,7 @@ void blob_add(typval_T *argvars, typval_T *rettv);
 void blob_remove(typval_T *argvars, typval_T *rettv, char_u *arg_errmsg);
 void blob_filter_map(blob_T *blob_arg, filtermap_T filtermap, typval_T *expr, char_u *arg_errmsg, typval_T *rettv);
 void blob_insert_func(typval_T *argvars, typval_T *rettv);
+void blob_extend_func(typval_T *argvars, char_u *arg_errmsg, int is_new, typval_T *rettv);
 void blob_reduce(typval_T *argvars, typval_T *expr, typval_T *rettv);
 void blob_reverse(blob_T *b, typval_T *rettv);
 void f_blob2list(typval_T *argvars, typval_T *rettv);
diff --git a/src/testdir/test_listdict.vim b/src/testdir/test_listdict.vim
index 88c2e07bd..9a8f807ec 100644
--- a/src/testdir/test_listdict.vim
+++ b/src/testdir/test_listdict.vim
@@ -1193,7 +1193,7 @@ func Test_listdict_compare_complex()
   call v9.CheckLegacyAndVim9Success(lines)
 endfunc
 
-" Test for extending lists and dictionaries
+" Test for extending lists, dictionaries and blobs
 func Test_listdict_extend()
   " Test extend() with lists
 
@@ -1273,8 +1273,59 @@ func Test_listdict_extend()
   call assert_fails("call extend(d, {'b': 0, 'c':'C'}, 1.2)", 'E475:')
   call assert_equal({'a': 'A', 'b': 'B'}, d)
 
-  call assert_fails("call extend([1, 2], 1)", 'E712:')
-  call assert_fails("call extend([1, 2], {})", 'E712:')
+  " Test extend() with blobs
+
+  " Pass the same Blob to extend()
+  let lines =<< trim END
+      VAR b = 0z010203
+      call assert_equal(0z010203010203, extend(b, b))
+      call assert_equal(0z010203010203, b)
+
+      LET b = 0z010203
+      call assert_equal(0z010203040506, extend(b, 0z040506))
+      call assert_equal(0z010203040506, b)
+
+      LET b = 0z010203
+      call extend(b, 0z040506, 0)
+      call assert_equal(0z040506010203, b)
+
+      LET b = 0z010203
+      call extend(b, 0z040506, 1)
+      call assert_equal(0z010405060203, b)
+
+      LET b = 0z010203
+      call extend(b, 0z040506, 3)
+      call assert_equal(0z010203040506, b)
+
+      LET b = 0z010203
+      call extend(b, 0z040506, -1)
+      call assert_equal(0z010204050603, b)
+
+      LET b = 0z010203
+      call extend(b, 0z040506, -3)
+      call assert_equal(0z040506010203, b)
+
+      LET b = 0z010203
+      call assert_equal(0z010203, b->extend(0z))
+      call assert_equal(0z010203, b->extend(test_null_blob()))
+      call assert_equal(0z010203, b)
+  END
+  call v9.CheckLegacyAndVim9Success(lines)
+
+  let b = 0z010203
+  call assert_fails("call extend(b, 0z040506, 4)", 'E979:')
+  call assert_fails("call extend(b, 0z040506, -4)", 'E979:')
+  call assert_fails("call extend(b, 0z040506, 1.2)", 'E805:')
+
+  lockvar b
+  call assert_fails("call extend(b, 0z040506)", 'E741:')
+  unlockvar b
+
+  call assert_fails('call extend(test_null_blob(), test_null_blob())', 'E1581:')
+  call assert_fails('call extendnew(test_null_blob(), 0z01)', 'E1581:')
+
+  call assert_fails("call extend([1, 2], 1)", 'E896:')
+  call assert_fails("call extend([1, 2], {})", 'E896:')
 
   " Extend g: dictionary with an invalid variable name
   call assert_fails("call extend(g:, {'-!' : 10})", 'E461:')
@@ -1295,6 +1346,23 @@ func Test_listdict_extend()
       call assert_equal([1, 5, 7, 1, 5, 7], l)
   END
   call v9.CheckLegacyAndVim9Success(lines)
+
+  " Extend a blob with itself.
+  let lines =<< trim END
+      VAR b = 0z010507
+      call extend(b, b, 0)
+      call assert_equal(0z010507010507, b)
+      LET b = 0z010507
+      call extend(b, b, 1)
+      call assert_equal(0z010105070507, b)
+      LET b = 0z010507
+      call extend(b, b, 2)
+      call assert_equal(0z010501050707, b)
+      LET b = 0z010507
+      call extend(b, b, 3)
+      call assert_equal(0z010507010507, b)
+  END
+  call v9.CheckLegacyAndVim9Success(lines)
 endfunc
 
 func Test_listdict_extendnew()
@@ -1321,6 +1389,18 @@ func Test_listdict_extendnew()
   let d2['c'] = 'C'
   call assert_equal({'a': {'b': 'B'}, 'c': 'C'}, d2)
   call assert_equal({'a': {'b': 'B'}}, d)
+
+  " Test extendnew() with blobs
+  let b = 0z010203
+  call assert_equal(0z0102030405, extendnew(b, 0z0405))
+  call assert_equal(0z010203, b)
+  lockvar b
+  call assert_equal(0z0102030405, extendnew(b, 0z0405))
+  let b2 = extendnew(b, test_null_blob())
+  call assert_equal(0z010203, b2)
+  let b2 += 0z04
+  call assert_equal(0z01020304, b2)
+  call assert_equal(0z010203, b)
 endfunc
 
 func s:check_scope_dict(x, fixed)
@@ -1606,6 +1686,7 @@ func Test_extendnew_leak()
   " This used to leak memory
   for i in range(100) | silent! call extendnew([], [], []) | endfor
   for i in range(100) | silent! call extendnew({}, {}, {}) | endfor
+  for i in range(100) | silent! call extendnew(0z, 0z, 0z) | endfor
 endfunc
 
 " Test for comparing deeply nested List/Dict values
diff --git a/src/testdir/test_tuple.vim b/src/testdir/test_tuple.vim
index 00428a5b0..de61fb09d 100644
--- a/src/testdir/test_tuple.vim
+++ b/src/testdir/test_tuple.vim
@@ -1755,9 +1755,9 @@ func Test_tuple_extend()
     call extendnew(t, (4, 5))
   END
   call v9.CheckSourceLegacyAndVim9Failure(lines, [
-        \ 'E712: Argument of extend() must be a List or Dictionary',
+        \ 'E896: Argument of extend() must be a List, Dictionary or Blob',
         \ 'E1013: Argument 1: type mismatch, expected list<any> but got tuple<number, number, number>',
-        \ 'E712: Argument of extend() must be a List or Dictionary'])
+        \ 'E896: Argument of extend() must be a List, Dictionary or Blob'])
 endfunc
 
 " Test for filter() with a tuple
diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim
index 49ab862d1..a32fe559b 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -1162,6 +1162,11 @@ def Test_extend_arg_types()
       assert_equal({a: 1, b: 2}, extend({a: 1, b: 2}, {b: 4}, 'keep'))
       assert_equal({a: 1, b: 2}, extend({a: 1, b: 2}, {b: 4}, g:string_keep))
 
+      assert_equal(0z010203, extend(0z0102, 0z03))
+      assert_equal(0z030102, extend(0z0102, 0z03, 0))
+      assert_equal(0z010302, extend(0z0102, 0z03, 1))
+      assert_equal(0z010302, extend(0z0102, 0z03, g:number_one))
+
       # mix of types is OK without a declaration
 
       var res: list<dict<any>>
@@ -1182,14 +1187,17 @@ def Test_extend_arg_types()
   END
   v9.CheckSourceDefAndScriptSuccess(lines)
 
-  v9.CheckSourceDefAndScriptFailure(['extend("a", 1)'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E712: Argument of extend() must be a List or Dictionary'])
-  v9.CheckSourceDefAndScriptFailure(['extend([1, 2], 3)'], ['E1013: Argument 2: type mismatch, expected list<any> but got number', 'E712: Argument of extend() must be a List or Dictionary'])
+  v9.CheckSourceDefAndScriptFailure(['extend("a", 1)'], ['E1013: Argument 1: type mismatch, expected list<any> but got string', 'E896: Argument of extend() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extend([1, 2], 3)'], ['E1013: Argument 2: type mismatch, expected list<any> but got number', 'E896: Argument of extend() must be a List, Dictionary or Blob'])
   v9.CheckSourceDefAndScriptFailure(['var ll = [1, 2]', 'extend(ll, ["x"])'], ['E1013: Argument 2: type mismatch, expected list<number> but got list<string>', 'E1013: Argument 2: type mismatch, expected list<number> but got list<string>'])
   v9.CheckSourceDefFailure(['extend([1, 2], [3], "x")'], 'E1013: Argument 3: type mismatch, expected number but got string')
 
   v9.CheckSourceDefFailure(['extend({a: 1}, 42)'], 'E1013: Argument 2: type mismatch, expected dict<any> but got number')
   v9.CheckSourceDefFailure(['extend({a: 1}, {b: 2}, 1)'], 'E1013: Argument 3: type mismatch, expected string but got number')
 
+  v9.CheckSourceDefFailure(['extend(0z01, 42)'], 'E1013: Argument 2: type mismatch, expected blob but got number')
+  v9.CheckSourceDefFailure(['extend(0z01, 0z02, 1.1)'], 'E1013: Argument 3: type mismatch, expected number but got float')
+
   v9.CheckSourceScriptFailure(['vim9script', 'var l = [1]', 'extend(l, ["b", 1])'], 'E1013: Argument 2: type mismatch, expected list<number> but got list<any> in extend()')
 enddef
 
@@ -1305,6 +1313,19 @@ def Test_extend_const()
   END
   v9.CheckSourceDefSuccess(lines)
 
+  lines =<< trim END
+      const b = 0z0102
+      extend(b, 0z03)
+  END
+  v9.CheckSourceDefFailure(lines, 'E1307: Argument 1: Trying to modify a const blob')
+
+  lines =<< trim END
+      final b = 0z0102
+      extend(b, 0z03)
+      assert_equal(0z010203, b)
+  END
+  v9.CheckSourceDefSuccess(lines)
+
   # item in a for loop is final
   lines =<< trim END
       var l: list<dict<any>> = [{n: 1}]
@@ -1318,11 +1339,15 @@ enddef
 def Test_extendnew()
   assert_equal([1, 2, 'a'], extendnew([1, 2], ['a']))
   assert_equal({one: 1, two: 'a'}, extendnew({one: 1}, {two: 'a'}))
-
-  v9.CheckSourceDefAndScriptFailure(['extendnew({a: 1}, 42)'], ['E1013: Argument 2: type mismatch, expected dict<number> but got number', 'E712: Argument of extendnew() must be a List or Dictionary'])
-  v9.CheckSourceDefAndScriptFailure(['extendnew({a: 1}, [42])'], ['E1013: Argument 2: type mismatch, expected dict<number> but got list<number>', 'E712: Argument of extendnew() must be a List or Dictionary'])
-  v9.CheckSourceDefAndScriptFailure(['extendnew([1, 2], "x")'], ['E1013: Argument 2: type mismatch, expected list<number> but got string', 'E712: Argument of extendnew() must be a List or Dictionary'])
-  v9.CheckSourceDefAndScriptFailure(['extendnew([1, 2], {x: 1})'], ['E1013: Argument 2: type mismatch, expected list<number> but got dict<number>', 'E712: Argument of extendnew() must be a List or Dictionary'])
+  assert_equal(0z010203, extendnew(0z0102, 0z03))
+
+  v9.CheckSourceDefAndScriptFailure(['extendnew({a: 1}, 42)'], ['E1013: Argument 2: type mismatch, expected dict<number> but got number', 'E896: Argument of extendnew() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extendnew({a: 1}, [42])'], ['E1013: Argument 2: type mismatch, expected dict<number> but got list<number>', 'E896: Argument of extendnew() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extendnew([1, 2], "x")'], ['E1013: Argument 2: type mismatch, expected list<number> but got string', 'E896: Argument of extendnew() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extendnew([1, 2], {x: 1})'], ['E1013: Argument 2: type mismatch, expected list<number> but got dict<number>', 'E896: Argument of extendnew() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extendnew(0z0102, "x")'], ['E1013: Argument 2: type mismatch, expected blob but got string', 'E896: Argument of extendnew() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extendnew(0z0102, [42])'], ['E1013: Argument 2: type mismatch, expected blob but got list<number>', 'E896: Argument of extendnew() must be a List, Dictionary or Blob'])
+  v9.CheckSourceDefAndScriptFailure(['extendnew(0z0102, 0z03, 1.1)'], ['E1013: Argument 3: type mismatch, expected number but got float', 'E805: Using a Float as a Number'])
 enddef
 
 def Test_feedkeys()
diff --git a/src/version.c b/src/version.c
index c03f5e545..68353e0ba 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    972,
 /**/
     971,
 /**/
diff --git a/src/vim9execute.c b/src/vim9execute.c
index c8790d634..b72e5700b 100644
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -2996,7 +2996,8 @@ execute_for(isn_T *iptr, ectx_T *ectx)
 	// changed.
 	if (idxtv->vval.v_number == -1 && blob != NULL)
 	{
-	    blob_copy(blob, ltv);
+	    ltv->v_lock = 0;
+	    ltv->vval.v_blob = blob_copy(blob);
 	    blob_unref(blob);
 	    blob = ltv->vval.v_blob;
 	}
@@ -4740,7 +4741,8 @@ exec_instructions(ectx_T *ectx)
 			tv->vval.v_float = iptr->isn_arg.fnumber;
 			break;
 		    case ISN_PUSHBLOB:
-			blob_copy(iptr->isn_arg.blob, tv);
+			tv->v_type = VAR_BLOB;
+			tv->vval.v_blob = blob_copy(iptr->isn_arg.blob);
 			break;
 		    case ISN_PUSHFUNC:
 			tv->v_type = VAR_FUNC;
diff --git a/src/vim9instr.c b/src/vim9instr.c
index 069818b35..a3e9243ca 100644
--- a/src/vim9instr.c
+++ b/src/vim9instr.c
@@ -1006,7 +1006,8 @@ generate_PUSHBLOB(cctx_T *cctx, blob_T *blob)
     isn_T	*isn;
 
     RETURN_OK_IF_SKIP(cctx);
-    if ((isn = generate_instr_type(cctx, ISN_PUSHBLOB, &t_blob)) == NULL)
+    if ((isn = generate_instr_type2(cctx, ISN_PUSHBLOB,
+					&t_blob, &t_blob)) == NULL)
 	return FAIL;
     isn->isn_arg.blob = blob;
 

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups "vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1wwQRh-00AhrE-HV%40256bit.org.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.