expl3 provides several do/while/repeat loops, but I'm failing to find a way to break such loops early.

What I want is something like (in C-style code)

while (true) {
    if (<condition_1>)
    <some code>

    if (<condition_2>)
    <some other code>

    if (<condition_n>)
    <even more code>

I could certainly define my own macros for that, but is there a way to use the functions already provided by the expl3 modules? Or alternatively, what's the idiomatic expl3 solution for this? \prg_break_point: looks promising, I have no idea how this should be used, though.

  • Are you talking about the \int_do_while:nn et al. functions? These cannot be escaped of early (not with fair play, at least ;-). They are recursive functions, for example: \cs_new:Npn \int_do_while:nn #1#2 { #2 \int_compare:nT {#1} { \int_do_while:nn {#1} {#2} } }, so they don't have the usual break points. Only map functions work with \prg_map_break:Nn Commented Nov 25, 2019 at 23:56
  • About the \bool_* versions, but I just saw they are defined similarily :(
    – siracusa
    Commented Nov 25, 2019 at 23:58
  • What would be the use case? Perhaps some other function fits the bill :-) Commented Nov 26, 2019 at 0:06
  • It's for a specific search over an intarray where I have to move "pointers" around until a certain set of conditions is satisfied. I can't use the simple loops here because the conditions depend on certain assertions (not being out of bounds etc.)
    – siracusa
    Commented Nov 26, 2019 at 0:13

2 Answers 2


Proof of concept, NOT to be taken seriously

You can be naughty and exploit the implementation details of the function you want to use to break it sooner. For instance, take \bool_do_while:Nn, which is defined as:

\cs_new:Npn \bool_do_while:Nn #1#2
  { #2 \bool_if:NT #1 { \bool_do_while:Nn #1 {#2} } }

The code you supply is executed right before a \bool_if:NT. You can define some macro which reverses this \bool_if:NT to \bool_if:NF and then returns the opposite (or one which consumes everything and ends everything in one blow). For example (I hope the name is suggestive enough ;-)

\cs_new:Npn \__siracusa_DONT_DO_THIS:w #1 \bool_if:NT { \bool_if:NF }

then you can use like:

\int_new:N \l_siracusa_tmp_int
\bool_do_while:Nn \c_true_bool
    \int_incr:N \l_siracusa_tmp_int
    % if (<condition>)
    \int_compare:nNnT { \l_siracusa_tmp_int } = { 6 }
      { \__siracusa_DONT_DO_THIS:w } % break;
    % <some code>
    Hello~\int_use:N \l_siracusa_tmp_int \par

You can have arbitrarily many conditions along the body of the loop, and issue the break code from anywhere as long as \bool_if:NT doesn't appear (unbraced, at least) in the code body. It could be made more robust by adding a marker at the end of the loop body, for example.

However this will only work for \bool_do_while:Nn, since it relies on how the macro is defined, and it goes without saying that should anything change in the implementation (which is possible), the code will break miserably.

  • I wonder about the usefulness of this... Perhaps something like \bool_break_do: could be added to allow more generic break control over the loops... Commented Nov 26, 2019 at 0:24
  • I'm in favor of adding such a command
    – siracusa
    Commented Nov 26, 2019 at 0:31
  • 1
    @siracusa Issue posted. I think it would be useful as well, and I don't immediately see a reason why not. Let's see what the others have to say. Commented Nov 26, 2019 at 0:43

For the record, this is the approach I'm using meanwhile:

\cs_new_protected:Npn \__foo_goto_loop_end:w #1 \__foo_loop_end: { }
\cs_new_protected:Npn \__foo_loop_end: { }

\bool_set_true:N \l_tmpa_bool
\int_set:Nn \l_tmpa_int { 1 }
\bool_do_while:Nn \l_tmpa_bool {
    \int_compare:nNnT \l_tmpa_int > 9 {
        \bool_set_false:N \l_tmpa_bool

    % Other conditions ...
    \int_compare:nNnT 0 = 1 {
        \bool_set_false:N \l_tmpa_bool

    \iow_term:x { iteration~\int_eval:n \l_tmpa_int }
    \int_gincr:N \l_tmpa_int


The code-skipping idea is similar to the one of Phelype Oleinik's answer, but it doesn't really break the loop, it just jumps to the end skipping all the remaining code of the loop body. So this doesn't rely on the internal definition of the loop macro and thus works with all loop variants.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .