@@ -324,6 +324,16 @@ class Logo {
324324 // Midi Data
325325 this . _midiData = { } ;
326326
327+ // Async yield mechanism: prevent infinite recursion from freezing the UI.
328+ // _syncCounter tracks consecutive synchronous block executions.
329+ // When it exceeds _YIELD_AFTER_SYNC_RUNS, we yield to the event loop
330+ // via setTimeout(0) so the browser can process paint/input events.
331+ // _totalIterations is a per-run safety limit to catch true infinite loops.
332+ this . _syncCounter = 0 ;
333+ this . _YIELD_AFTER_SYNC_RUNS = 1000 ;
334+ this . _totalIterations = 0 ;
335+ this . _MAX_ITERATIONS = 1000000 ;
336+
327337 // When running in step-by-step mode, the next command to run
328338 // is queued here.
329339 this . stepQueue = { } ;
@@ -1128,6 +1138,10 @@ class Logo {
11281138
11291139 this . stopTurtle = false ;
11301140
1141+ // Reset async yield counters for this run.
1142+ this . _syncCounter = 0 ;
1143+ this . _totalIterations = 0 ;
1144+
11311145 this . activity . blocks . unhighlightAll ( ) ;
11321146 this . activity . blocks . bringToTop ( ) ; // Draw under the blocks.
11331147
@@ -1399,6 +1413,10 @@ class Logo {
13991413
14001414 this . receivedArg = receivedArg ;
14011415
1416+ // Reset async yield counters – execution will go through
1417+ // setTimeout below, giving the event loop a chance to breathe.
1418+ logo . _syncCounter = 0 ;
1419+
14021420 const tur = logo . activity . turtles . ithTurtle ( turtle ) ;
14031421
14041422 const delay = logo . turtleDelay + tur . waitTime ;
@@ -1437,6 +1455,22 @@ class Logo {
14371455
14381456 this . receivedArg = receivedArg ;
14391457
1458+ // Safety check: stop execution if we have exceeded the maximum
1459+ // iteration limit, which indicates an infinite loop (e.g., an
1460+ // Action block that calls itself with no delay).
1461+ logo . _totalIterations ++ ;
1462+ if ( logo . _totalIterations > logo . _MAX_ITERATIONS ) {
1463+ logo . activity . errorMsg (
1464+ _ ( "Infinite loop detected. Execution stopped to prevent browser freeze." ) ,
1465+ blk
1466+ ) ;
1467+ logo . stopTurtle = true ;
1468+ logo . _alreadyRunning = false ;
1469+ logo . _syncCounter = 0 ;
1470+ logo . _totalIterations = 0 ;
1471+ return ;
1472+ }
1473+
14401474 // Sometimes we don't want to unwind the entire queue.
14411475 if ( queueStart === undefined ) queueStart = 0 ;
14421476
@@ -1779,7 +1813,27 @@ class Logo {
17791813 }
17801814
17811815 if ( isflow ) {
1782- logo . runFromBlockNow ( logo , turtle , nextBlock , isflow , passArg , queueStart ) ;
1816+ // Async yield: periodically yield to the event loop so the
1817+ // browser can process paint/input events and the UI does not
1818+ // freeze during long-running or infinitely-recursive programs.
1819+ logo . _syncCounter ++ ;
1820+ if ( logo . _syncCounter >= logo . _YIELD_AFTER_SYNC_RUNS ) {
1821+ logo . _syncCounter = 0 ;
1822+ setTimeout ( ( ) => {
1823+ if ( ! logo . stopTurtle ) {
1824+ logo . runFromBlockNow (
1825+ logo ,
1826+ turtle ,
1827+ nextBlock ,
1828+ isflow ,
1829+ passArg ,
1830+ queueStart
1831+ ) ;
1832+ }
1833+ } , 0 ) ;
1834+ } else {
1835+ logo . runFromBlockNow ( logo , turtle , nextBlock , isflow , passArg , queueStart ) ;
1836+ }
17831837 } else {
17841838 logo . runFromBlock ( logo , turtle , nextBlock , isflow , passArg ) ;
17851839 }
0 commit comments