Thank you Joseph for the quick bug fix. Let me propose the following code for inclusion. The idea is similar to `\prg_new_map_functions`, which defines a set of maps, some of which are expandable. Namely, \tl_gset_replacer:nnn {<name>} {<item1>} {<item2>} defines expandable replacement functions: \<name>_replace_aux:nwwn \<name>_replace_some:nn \<name>_replace_one:n \<name>_replace_all:n which are all `f`-expandable. Usage: `\<name>_replace_some:nn {<int>} {<tl>}` will replace the <int> first occurences of <item1> by <item2> in <tl> (this happens in two steps). The `_all` version replaces all occurences, and the `_one` version replaces only the first occurence (both do that in three steps). The code follows. Sorry that it is made a bit obscure to avoid losing braces. \cs_new:Npn \quark_use_none_from_mark_to_stop:w #1 \q_mark #2 \q_stop {#1} \cs_new_eq:NN \_tl_storage:n \use:n \cs_new:Npn \_tl_store:n #1 #2 \_tl_storage:n #3 { #2 \_tl_storage:n {#3#1} } \cs_new:Npn \tl_gset_replacer_aux:Nnn #1 #2 #3 { \cs_gset:Npn #1 ##1 ##2 #2 ##3 \_tl_storage:n { \tl_if_empty:nTF {##3} { %if end, stop. \exp_after:wN \_tl_store:n \exp_after:wN {##2\q_stop} \_tl_storage:n } { \int_compare:nNnTF {##1}={0} { %if 0, stop. \exp_after:wN \_tl_store:n \exp_after:wN {##2 #2 ##3\q_stop} \_tl_storage:n }{ \exp_after:wN \_tl_store:n \exp_after:wN {##2 #3} \exp_after:wN #1 \exp_after:wN { \tex_number:D \etex_numexpr:D ##1-1 \tex_relax:D } \c_empty_tl ##3 \_tl_storage:n } } } } \cs_generate_variant:Nn \tl_gset_replacer_aux:Nnn {cnn} \cs_new:Npn \tl_gset_replacer:nnn #1 #2 #3 { \cs_gset:cpx {#1_replace_one:n} {\exp_not:c{#1_replace_some:nn} {1}} \cs_gset:cpx {#1_replace_all:n} {\exp_not:c{#1_replace_some:nn} {-1}} \cs_gset:cpn {#1_replace_some:nn} ##1 ##2 { \tex_romannumeral:D 0 \use:c {#1_replace_aux:nwwn} {##1} \c_empty_tl ##2 \q_mark #2 \_tl_storage:n{\quark_use_none_from_mark_to_stop:w \exp_stop_f:} } \tl_gset_replacer_aux:cnn {#1_replace_aux:nwwn} {#2} {#3} }