engine: make @dolist/now run multi-command bodies and honor @break (#765)

@dolist/now executed each iteration's body via a single process_command(),
so it could not split a ';'-separated body and never honored break_called.
Worse, running the body inline could mutate the executor's own attributes
and free the lbufs the 'list'/'command' arguments pointed into, corrupting
the walk so the loop stopped after the first iteration. Net effect: the
"@break stops the loop" idiom the assert_cmd tests wanted was impossible,
and @dolist/now over an accumulating body silently truncated.

do_dolist / bind_and_process now (for the /now path):
  - iterate over private copies of the list and command so inline body
    execution can't pull the rug out from under the walk,
  - run the body as an action list (split on ';', honoring break_called),
    mirroring do_include(), and
  - stop the loop when @break/@assert fires, while saving/restoring
    break_called so the break is contained to the dolist and does not leak
    into the surrounding command list.

The queued (default) @dolist path is unchanged in behavior.

Adds assert_cmd TC005/006/007 to lock in the inline semantics: break
stops the loop (-> "a"), a non-firing loop records every element
(-> "a b c", the truncation regression guard), and an inline break stays
contained (a command after @dolist/now in the same list still runs).

Verified: full smoke 1081 ok / 0 new failures; standalone repros confirm
break->"a", no-break->"a b c", and outer-command-still-runs. Closes the
follow-up noted in #765.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stephen Dennis 2026-06-07 14:44:36 -05:00
parent 7095dbfdd5
commit 37e18bef52
2 changed files with 121 additions and 3 deletions

View file

@ -40,7 +40,27 @@ static void bind_and_process(dbref executor, dbref caller, dbref enactor,
mudstate.inum[mudstate.in_loop] = number;
}
mudstate.in_loop++;
process_command(executor, caller, enactor, eval, false, action, cargs, ncargs);
// Execute the body as an action list: split on ';' and run each command
// inline, honoring @break/@assert (break_called). parse_to() rewrites its
// buffer in place, so work on a private copy — do_dolist reuses the same
// 'action' buffer for every iteration. Mirrors do_include().
//
LBuf tbuf = LBuf_Src("dolist.now");
mux_strncpy(tbuf, action, LBUF_SIZE - 1);
UTF8 *command = tbuf.get();
while ( command
&& !break_called)
{
UTF8 *cp = parse_to(&command, ';', EV_STRIP_AROUND);
if ( cp
&& *cp)
{
process_command(executor, caller, enactor, eval, false, cp,
cargs, ncargs);
}
}
mudstate.in_loop--;
if (bLoopInBounds)
{
@ -79,6 +99,33 @@ void do_dolist(dbref executor, dbref caller, dbref enactor, int eval, int key,
}
delimiter = *tempstr;
}
// Inline (/now) bodies run synchronously and may mutate the executor's
// own attributes, which can free the lbufs that 'list' and 'command' (the
// @dolist arguments) point into and corrupt our walk. Iterate over
// private copies so body execution cannot pull the rug out from under us.
// (The queued path is immune — wait_que() copies the body — but copying
// for both keeps the loop uniform and an lbuf alloc is a freelist pop.)
//
LBuf listcopy = LBuf_Src("dolist.list");
LBuf cmdcopy = LBuf_Src("dolist.cmd");
mux_strncpy(listcopy, curr, LBUF_SIZE - 1);
curr = listcopy.get();
if (nullptr != command)
{
mux_strncpy(cmdcopy, command, LBUF_SIZE - 1);
command = cmdcopy.get();
}
// For inline (/now) iteration, isolate @break/@assert to the loop: save
// and clear break_called so a break inside the body stops the dolist
// without leaking to the surrounding command list, then restore it.
//
bool save_break = break_called;
if (key & DOLIST_NOW)
{
break_called = false;
}
while (curr && *curr)
{
while (*curr == delimiter)
@ -93,6 +140,12 @@ void do_dolist(dbref executor, dbref caller, dbref enactor, int eval, int key,
{
bind_and_process(executor, caller, enactor, eval, command,
objstring, cargs, ncargs, number);
if (break_called)
{
// @break/@assert fired inside the body; stop the loop.
//
break;
}
}
else
{
@ -101,6 +154,10 @@ void do_dolist(dbref executor, dbref caller, dbref enactor, int eval, int key,
}
}
}
if (key & DOLIST_NOW)
{
break_called = save_break;
}
if (key & DOLIST_NOTIFY)
{

View file

@ -90,11 +90,72 @@
{
@if strmatch(setr(0,trim(get(me/tc004_result))),a c)=
{
@log smoke=TC004: assert false skips matched element. Succeeded.;
@log smoke=TC004: assert false skips matched element. Succeeded.
},
{
@log smoke=TC004: assert false skips matched element. Failed (%q0).
}
}
-
#
# Inline (@dolist/now) cases: @break/@assert stop the whole loop, not just
# the current element. break on 'b' -> only 'a' recorded.
#
# Test Case #5 - @break true stops the inline loop ('b' onward dropped).
#
&tr.tc005 test_assert_cmd=
&tc005_result me=;
@dolist/now a b c=
{@break/inline strmatch(##,b);&tc005_result me=[get(me/tc005_result)] ##};
@wait 0.5=
{
@if strmatch(setr(0,trim(get(me/tc005_result))),a)=
{
@log smoke=TC005: break stops inline dolist. Succeeded.
},
{
@log smoke=TC005: break stops inline dolist. Failed (%q0).
}
}
-
#
# Test Case #6 - @break false never fires; inline loop records every element.
# (Regression guard: inline iteration must not stop early.)
#
&tr.tc006 test_assert_cmd=
&tc006_result me=;
@dolist/now a b c=
{@break/inline 0;&tc006_result me=[get(me/tc006_result)] ##};
@wait 0.5=
{
@if strmatch(setr(0,trim(get(me/tc006_result))),a b c)=
{
@log smoke=TC006: inline dolist records all. Succeeded.
},
{
@log smoke=TC006: inline dolist records all. Failed (%q0).
}
}
-
#
# Test Case #7 - inline break stops the loop but stays contained: a command
# after the @dolist/now in the same list still runs.
#
&tr.tc007 test_assert_cmd=
&tc007_result me=;
&tc007_after me=;
@dolist/now a b c=
{@break/inline strmatch(##,b);&tc007_result me=[get(me/tc007_result)] ##};
&tc007_after me=DONE;
@wait 0.5=
{
@if and(strmatch(trim(get(me/tc007_result)),a),strmatch(get(me/tc007_after),DONE))=
{
@log smoke=TC007: inline break is contained. Succeeded.;
@trig me/tr.done
},
{
@log smoke=TC004: assert false skips matched element. Failed (%q0).;
@log smoke=TC007: inline break is contained. Failed (r=[trim(get(me/tc007_result))] after=[get(me/tc007_after)]).;
@trig me/tr.done
}
}