mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
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:
parent
7095dbfdd5
commit
37e18bef52
2 changed files with 121 additions and 3 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue