Looking into this more closely, there’s a chain of events that cause this problem to emerge, and I’m not sure that altering end the story saying... really gets at the root issue.
The first key events are caused by the Instead of jumping... rule:
! No specific request
! Instead of jumping:
[ R_971 I7RBLK;
@push I7SFRAME;
StackFrameCreate(2);
BlkValueCreateOnStack(0, TEXT_TY); ! <-- step 1
I7RBLK = KERNEL_1();
BlkValueFreeOnStack(0);
@pull I7SFRAME;
return I7RBLK; ! nothing
];
The R_971() code sets up a space for a block value (in this case a text) on the stack (step 1).
[ KERNEL_1 ;
if ((((action ==##Jump) && (actor==player)))) { ! Runs only when pattern matches
self = noun;
if (debug_rules) DB_Rule(R_971, 971);
! [2: say ~X[note footnote-placeholder][paragraph break]~]
say__p=1;! [3: ~X~]
ParaContent(); print "X";! [4: note footnote-placeholder]
ParaContent(); (PHR_970_r2 (I182_footnote_placeholder));! [5: paragraph break]
ParaContent(); DivideParagraphPoint(); new_line; .L_Say3; .L_SayX3;! [6: end the story with ~foobar~]
(PHR_968_r3 (BlkValueCopy(I7SFRAME, TX_L_55))); ! <-- step 2
RulebookFails(); rtrue;
} else if (debug_rules > 1) DB_Rule(R_971, 971, 'action');
rfalse;
];
The subsidiary routine KERNEL_1() then uses that space on the stack as temporary storage for the argument (step 2) that will be passed to the routine compiled for the custom end the story with... phrase. It does this via…
[ BlkValueCopy to_bv from_bv to_kind from_kind kovs;
if (to_bv == 0) BlkValueError("copy to null value");
if (from_bv == 0) BlkValueError("copy from null value");
if (to_bv == from_bv) return;
#ifdef BLKVALUE_TRACE;
print "Copy: ", (BlkValueDebug) to_bv, " to equal ", (BlkValueDebug) from_bv, "^";
#endif;
to_kind = BlkValueWeakKind(to_bv);
from_kind = BlkValueWeakKind(from_bv);
if (to_kind ~= from_kind) BlkValueError("copy incompatible kinds");
kovs = KOVSupportFunction(to_kind, "impossible copy");
if (kovs(COPYQUICK_KOVS, to_bv, from_bv))
BlkValueQuickCopyPrimitive(to_bv, from_bv, kovs); ! <-- always this case for TEXT_TY
else
BlkValueSlowCopyPrimitive(to_bv, from_bv, kovs, true);
return to_bv; ! <-- step 3
];
which returns the value of the to_bv argument (step 3). This is the now-valid on-stack block value, which has been given the same underlying long block as from_bv, i.e. it is a “by value” copy (I think) of the text value being fed as a parameter to PHR_968_r3(), which is what’s generated for the new end the story with... phrase. The value being copied (a short block) is still a kind of reference, though, one which points to the long block holding the actual text information.
! Request 3: phrase text -> nothing
! to end the story with ( t - text ):
[ PHR_968_r3
t_0 ! Call parameter 't': text
;
! [2: end the story saying t]
deadflag=t_0; story_complete=false; ! <-- step 4
rfalse;
];
The I6 code for end the story saying... is injected directly into the I7 phrase (for end the story with...) that calls it. When the just-created on-stack short block value is passed to the end the story with... phrase, the I6 code does a direct pass-through assignment to deadflag. What’s being assigned is the to_bv address returned from BlkValueCopy(), which is still the same value as when it was passed as the to_bv argument to BlkValueCopy() and which was originally set up by R_971()/KERNEL_1(), i.e. Instead of jumping....
The proximal cause of rileypb’s issue is that the -by-reference: requirement for the end the story saying... phrase is making it copy the (only temporarily-valid) text object address it has been given to deadflag. However, it seems to me that maybe part of the fault is also the way that R_971()/KERNEL_1() is letting a pointer to a location on the stack “escape” the confines of the local routine. The short block of such a block value is guaranteed to be invalidated (even if not immediately overwritten) when the local stack frame is torn down on the way out of the routine, so it’s asking for trouble to hand out a pointer to something within it.