brython-dev / brython

Brython (Browser Python) is an implementation of Python 3 running in the browser

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Incorrectly thrown `maximum recursion depth exceeded`

Daniel528 opened this issue · comments

Here is a stackblits with the error (open the developer tool console): https://stackblitz.com/edit/stackblitz-starters-dqwyi5?file=index.html

Alternatively here is the HTML file. See my (potential incorrect) explanation of the issue below

<html>
<head>
    <meta charset="utf-8">
    </script>
    
    <script src="
    https://cdn.jsdelivr.net/npm/brython@3/brython.min.js
    " crossorigin="anonymous"></script>
    <script src="
    https://cdn.jsdelivr.net/npm/brython@3/brython_stdlib.js
    " crossorigin="anonymous"></script>
</head>

<body>
<script type="text/javascript">
function wait(milliseconds) {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

class JSUtil {
  async delay(s) {
    await wait(s * 1000);
  }
  secondsToMilliseconds(s){
    return s/1000
  }
}

window.util = new JSUtil();
</script>
<script type="text/python">
from browser import aio, window
import sys

depth = 1001

async def asyncWrapper(f, *args):
  try:
    # do some other work
    r = await f(*args)
    return r
  except Exception as exc:
    # do some other work
    raise Exception(exc)

def syncWrapper(f, *args):
  try:
    # do some other work
    r = f(*args)
    return r
  except Exception as exc:
    # do some other work
    raise Exception(exc)

def delayWithSyncWrapper(s):
  return syncWrapper(window.util.delay, s)

def delayWithAsyncWrapper(s):
  return asyncWrapper(window.util.delay,s)

def secondsToMilliseconds(s):
  return syncWrapper(window.util.secondsToMilliseconds, s)

async def main():
  print("Test 1: normal sync function wrapped")
  x = 1
  while x < depth:
    print(secondsToMilliseconds(x))
    x = x + 1

  print("Test 2: async wrapper, async function")
  i = 1
  while i < depth:
    print(i)
    i = i + 1
    await delayWithAsyncWrapper(0.0001)
  

  print("Test 3: sync wrapper, async function start")
  i = 1
  while i < depth:
    print(i)
    i = i + 1
    await delayWithSyncWrapper(0.0001)

print("Begin Python Execution")
aio.run(main())
</script>
</body>

</html>

In the above text/python script being executed the main loop has 3 tests:

  • Test 1 is a loop running a sync function through a sync wrapper function.
  • Test 2 is a loop running an async function through a wrapper that has the async/await keywords applied to it
  • Test 3 (which contains the bug) is an async function run through the sync wrapper.

In none of the tests/wrappers is there any recursion. But the recursion warning is being thrown in test 3. From what I can tell inside www/src/py_utils.js the $B.enter_frame function checks the frame_obj count to check recursion depth. In the case of test 2, the frame logic properly handles the passing of the previous frame in this line of www/src/async.js.

In test 3 it doesn't do this. I would theorise the js<->py conversion is not correctly marking the returned promise in Test 3 through the wrapper as an async function and instead treating it as something else. This means the assigning of the previous frame doesn't happen in the resolution of the promise and then the count begins to spiral out of control. It eventually breaches the 1000 cap and throws the error.

This is my best guess after trying to comprehend the frame_obj functionality in the past day or so. From what I can tell, python3 does not need the explicit declaring of an the async/await keywords as is done in Test 2 and is able to instead treat the returned async function through the wrappers as an async function. I do not know how to resolve this any ideas are appreciated.

Welcome to the club. 🥲 I have encountered the same problem: #2390.

So further investigation. I think it comes down to the ast_to_js.js file, specifically this line not being run for the syncWrapper as there's no async keyword.

So as a result, the coroutine class is never made here. Then that means the promise never sends and this frame restoration never takes place.

My second attempt at fixing the bug seems to work, but be careful with the jumping for joy, it's late and I'm tired, so could be that my fix is just garbage and apart from that I still need to check if there are any unwanted side effects.

The snapshot is attached.
brython.zip

@Daniel528
The issue should be fixed by @moepnse 's PR referenced above. Can you confirm so we can close the ticket ?

Have tested the above stackblitz environment against the latest https://raw.githack.com/brython-dev/brython/master/www/src/* files which from what I can see contain the merge request. Thankfully the bug didn't appear.

Also tested against our app's production usage of brython and the recurssion depth exceeded error is no longer being thrown incorrectly there. I'd say it's solved. Thanks a bunch @moepnse & @PierreQuentel for the effort.