@@ -392,7 +392,13 @@ public boolean find() {
392392 calloutHandler = new PerlCalloutHandler (input , byteToChar , callbacks , flags );
393393 matcher .setCalloutHandler (calloutHandler );
394394 }
395- int result = matcher .search (charToByte [nextStart ], charToByte [regionEnd ], Option .NONE );
395+ int result ;
396+ try {
397+ result = matcher .search (charToByte [nextStart ], charToByte [regionEnd ], Option .NONE );
398+ } catch (RuntimeException | Error failure ) {
399+ if (calloutHandler != null ) calloutHandler .abort ();
400+ throw failure ;
401+ }
396402 matched = result >= 0 ;
397403 if (calloutHandler != null ) calloutHandler .finish (matched );
398404 if (!matched ) return false ;
@@ -511,6 +517,7 @@ private record Token(int localLevel, RegexState regexState, RuntimeScalar previo
511517 private final List <RuntimeRegexCallback > callbacks ;
512518 private final RegexFlags outerFlags ;
513519 private final RegexCallbackMutationSnapshot mutations ;
520+ private final int initialLocalLevel ;
514521 private RuntimeScalar completedResult ;
515522
516523 PerlCalloutHandler (String input , int [] byteToChar , List <RuntimeRegexCallback > callbacks ,
@@ -528,6 +535,7 @@ private PerlCalloutHandler(String input, int[] byteToChar,
528535 this .callbacks = callbacks ;
529536 this .outerFlags = outerFlags ;
530537 this .mutations = mutations ;
538+ this .initialLocalLevel = DynamicVariableManager .getLocalLevel ();
531539 for (RuntimeRegexCallback callback : callbacks ) mutations .include (callback .code );
532540 }
533541
@@ -589,8 +597,17 @@ private Evaluation evaluate(RuntimeRegexCallback callback, MatchView match) {
589597 publishProvisional (match );
590598
591599 try {
592- RuntimeScalar result = RuntimeCode .apply (new RuntimeScalar (callback .code ),
593- new RuntimeArray (), RuntimeContextType .SCALAR ).scalar ();
600+ DynamicVariableManager .CapturedFrame <RuntimeList > frame =
601+ DynamicVariableManager .captureFrameLocals (() -> RuntimeCode .apply (
602+ new RuntimeScalar (callback .code ), new RuntimeArray (),
603+ RuntimeContextType .SCALAR ));
604+ // Joni's complete() notification is delayed until the candidate
605+ // path commits. Resume now so a later (?{ ... }) on that same
606+ // path observes local() values; unwind() still owns the token's
607+ // pre-callback level and rolls the frame back on backtracking.
608+ DynamicVariableManager .resumeSuspended (frame .states ());
609+ rejectEscapedControlFlow (frame .result ());
610+ RuntimeScalar result = frame .result ().scalar ();
594611 boolean block = callback .kind == RuntimeRegexCallback .Kind .BLOCK ;
595612 if (block ) rVariable .set (result );
596613 Token token = new Token (localLevel , savedRegex , previousR ,
@@ -617,15 +634,32 @@ public void complete(Object value) {
617634 }
618635
619636 void finish (boolean matched ) {
620- if (!matched ) mutations .restore ();
621- if (matched && completedResult != null ) {
622- GlobalVariable .getGlobalVariable (GlobalContext .encodeSpecialVar ("R" ))
623- .set (completedResult );
637+ try {
638+ if (!matched ) mutations .restore ();
639+ if (matched && completedResult != null ) {
640+ GlobalVariable .getGlobalVariable (GlobalContext .encodeSpecialVar ("R" ))
641+ .set (completedResult );
642+ }
643+ } finally {
644+ DynamicVariableManager .popToLocalLevel (initialLocalLevel );
645+ }
646+ }
647+
648+ void abort () {
649+ try {
650+ mutations .restore ();
651+ } finally {
652+ DynamicVariableManager .popToLocalLevel (initialLocalLevel );
624653 }
625654 }
626655
627656 private void restore (Token token , boolean completed ) {
628- restoreCallbackScope (token .localLevel (), token .regexState (), token .previousR ());
657+ if (!completed ) {
658+ DynamicVariableManager .popToLocalLevel (token .localLevel ());
659+ }
660+ token .regexState ().restore ();
661+ GlobalVariable .getGlobalVariable (GlobalContext .encodeSpecialVar ("R" ))
662+ .set (token .previousR ());
629663 if (completed && token .block () && completedResult == null ) {
630664 completedResult = token .result ();
631665 }
@@ -642,6 +676,21 @@ private static void restoreCallbackScope(int localLevel, RegexState regexState,
642676 }
643677 }
644678
679+ private static void rejectEscapedControlFlow (RuntimeList result ) {
680+ if (!(result instanceof RuntimeControlFlowList flow )) return ;
681+ ControlFlowMarker marker = flow .marker ;
682+ if (marker .type == ControlFlowType .GOTO
683+ || marker .type == ControlFlowType .TAILCALL ) {
684+ // The runtime location is the regex pseudo-block boundary (and
685+ // can differ from the marker's inner goto location), so let the
686+ // exception formatter attach it.
687+ throw new PerlCompilerException ("Can't \" goto\" out of a pseudo block" );
688+ }
689+ // Preserve the control op's own location. The terminating newline
690+ // tells PerlCompilerException this is already fully formatted.
691+ throw new PerlCompilerException (marker .buildErrorMessage () + ".\n " );
692+ }
693+
645694 private void publishProvisional (MatchView match ) {
646695 RuntimeRegexState state = PerlRuntime .current ().regexState ;
647696 int count = match .captureCount ();
0 commit comments