This fixes a bug in the previous version where if the code does not use top level await but returns a generator or coroutine, the code would try to await it. This sometimes causes strange errors or other unexpected behaviors.

Instead, I switched to a new method of returning by raising an exception. This makes it so the coroutines produced by top level await are returned via a separate channel from return values from the code so we won't confuse them anymore.

I added a new class Executor. On creation, the Executor is given the source code and config options and produces an AST. The caller can then manipulate the AST as they see fit before calling `compile` and then `run` or `run_async`. This passes to the user the most powerful possible interface, and should make it so that Executor is helpful even in complicated use cases.

I put the main parse/compile logic into a separate generator to avoid storing any of the config options on the Executor class. This helps separate out control logic from config options and limit the scope of variables as far as possible.
ENH Improvements to eval_code. This fixes a bug in the previous version where if the code does not use top level await but returns a generator or coroutine, the code would try to await it. This sometimes causes strange errors or other unexpected behaviors.

Instead, I switched to a new method of returning by raising an exception. This makes it so the coroutines produced by top level await are returned via a separate channel from return values from the code so we won't confuse them anymore.

I added a new class Executor. On creation, the Executor is given the source code and config options and produces an AST. The caller can then manipulate the AST as they see fit before calling `compile` and then `run` or `run_async`. This passes to the user the most powerful possible interface, and should make it so that Executor is helpful even in complicated use cases.

I put the main parse/compile logic into a separate generator to avoid storing any of the config options on the Executor class. This helps separate out control logic from config options and limit the scope of variables as far as possible.
import json
from pathlib import Path
import re
import subprocess
import sys
from time import time

sys.path.insert(0, str((Path(__file__).resolve().parents[1] / "test")))
sys.path.insert(0, str((Path(__file__).resolve().parents[1])))

import conftest  # noqa: E402


SKIP = set(["fft", "hyantes", "README"])


def print_entry(name, res):
    print(" - ", name)
    print(" " * 4, end="")
    for name, dt in res.items():
        print("{}: {:.6f}  ".format(name, dt), end="")
    print("")


def run_native(hostpython, code):
    root = Path(__file__).resolve().parents[1]
    output = subprocess.check_output(
        [hostpython.resolve(), "-c", code],
        cwd=Path(__file__).resolve().parent,
        env={
            "PYTHONPATH": str(root / "src")
            + ":"
            + str(root / "packages" / ".artifacts" / "lib" / "python")
        },
    )
    return float(output.strip().split()[-1])


def run_wasm(code, selenium):
    selenium.run(code)
    try:
        runtime = float(selenium.logs.split("\n")[-1])
    except ValueError:
        print(selenium.logs)
        raise
    return runtime


def run_all(hostpython, selenium_backends, code):
    a = run_native(hostpython, code)
    result = {"native": a}
    for browser_name, selenium in selenium_backends.items():
        dt = run_wasm(code, selenium)
        result[browser_name] = dt
    return result


def get_pystone_benchmarks():
    yield "pystone", ("import pystone\n" "pystone.main(pystone.LOOPS)\n")


def parse_numpy_benchmark(filename):
    lines = []
    with open(filename) as fp:
        for line in fp:
            m = re.match(r"^#\s*(setup|run): (.*)$", line)
            if m:
                line = "{} = {!r}\n".format(m.group(1), m.group(2))
            lines.append(line)
    return "".join(lines)


def get_numpy_benchmarks():
    root = Path(__file__).resolve().parent / "benchmarks"
    for filename in sorted(root.iterdir()):
        name = filename.stem
        if name in SKIP:
            continue
        content = parse_numpy_benchmark(filename)
        content += (
            "import numpy as np\n"
            "_ = np.empty(())\n"
            "setup = setup + '\\nfrom __main__ import {}'\n"
            "from timeit import Timer\n"
            "t = Timer(run, setup)\n"
            "r = t.repeat(11, 40)\n"
            "r.remove(min(r))\n"
            "r.remove(max(r))\n"
            "print(np.mean(r))\n".format(name)
        )
        yield name, content


def get_benchmarks():
    yield from get_pystone_benchmarks()
    yield from get_numpy_benchmarks()


def main(hostpython):
    with conftest.spawn_web_server() as (hostname, port, log_path):
        results = {}
        selenium_backends = {}

        b = {"native": float("NaN")}
        browser_cls = [
            ("firefox", conftest.FirefoxWrapper),
            ("chrome", conftest.ChromeWrapper),
        ]
        for name, cls in browser_cls:
            t0 = time()
            selenium_backends[name] = cls(port)
            b[name] = time() - t0
            # pre-load numpy for the selenium instance used in benchmarks
            selenium_backends[name].load_package("numpy")
        results["selenium init"] = b
        print_entry("selenium init", b)

        # load packages
        for package_name in ["numpy"]:
            b = {"native": float("NaN")}
            for browser_name, cls in browser_cls:
                selenium = cls(port)
                try:
                    t0 = time()
                    selenium.load_package(package_name)
                    b[browser_name] = time() - t0
                finally:
                    selenium.driver.quit()
            results["load " + package_name] = b
            print_entry("load " + package_name, b)

        for name, content in get_benchmarks():
            results[name] = run_all(hostpython, selenium_backends, content)
            print_entry(name, results[name])
        for selenium in selenium_backends.values():
            selenium.driver.quit()
    return results


if __name__ == "__main__":
    results = main(Path(sys.argv[-2]).resolve())
    with open(sys.argv[-1], "w") as fp:
        json.dump(results, fp)

import json
from pathlib import Path
import re
import subprocess
import sys
from time import time

sys.path.insert(0, str((Path(__file__).resolve().parents[1] / "test")))
sys.path.insert(0, str((Path(__file__).resolve().parents[1])))

import conftest  # noqa: E402


SKIP = set(["fft", "hyantes", "README"])


def print_entry(name, res):
    print(" - ", name)
    print(" " * 4, end="")
    for name, dt in res.items():
        print("{}: {:.6f}  ".format(name, dt), end="")
    print("")


def run_native(hostpython, code):
    root = Path(__file__).resolve().parents[1]
    output = subprocess.check_output(
        [hostpython.resolve(), "-c", code],
        cwd=Path(__file__).resolve().parent,
        env={
            "PYTHONPATH": str(root / "src")
            + ":"
            + str(root / "packages" / ".artifacts" / "lib" / "python")
        },
    )
    return float(output.strip().split()[-1])


def run_wasm(code, selenium):
    selenium.run(code)
    try:
        runtime = float(selenium.logs.split("\n")[-1])
    except ValueError:
        print(selenium.logs)
        raise
    return runtime


def run_all(hostpython, selenium_backends, code):
    a = run_native(hostpython, code)
    result = {"native": a}
    for browser_name, selenium in selenium_backends.items():
        dt = run_wasm(code, selenium)
        result[browser_name] = dt
    return result


def get_pystone_benchmarks():
    yield "pystone", ("import pystone\n" "pystone.main(pystone.LOOPS)\n")


def parse_numpy_benchmark(filename):
    lines = []
    with open(filename) as fp:
        for line in fp:
            m = re.match(r"^#\s*(setup|run): (.*)$", line)
            if m:
                line = "{} = {!r}\n".format(m.group(1), m.group(2))
            lines.append(line)
    return "".join(lines)


def get_numpy_benchmarks():
    root = Path(__file__).resolve().parent / "benchmarks"
    for filename in sorted(root.iterdir()):
        name = filename.stem
        if name in SKIP:
            continue
        content = parse_numpy_benchmark(filename)
        content += (
            "import numpy as np\n"
            "_ = np.empty(())\n"
            "setup = setup + '\\nfrom __main__ import {}'\n"
            "from timeit import Timer\n"
            "t = Timer(run, setup)\n"
            "r = t.repeat(11, 40)\n"
            "r.remove(min(r))\n"
            "r.remove(max(r))\n"
            "print(np.mean(r))\n".format(name)
        )
        yield name, content


def get_benchmarks():
    yield from get_pystone_benchmarks()
    yield from get_numpy_benchmarks()


def main(hostpython):
    with conftest.spawn_web_server() as (hostname, port, log_path):
        results = {}
        selenium_backends = {}

        b = {"native": float("NaN")}
        browser_cls = [
            ("firefox", conftest.FirefoxWrapper),
            ("chrome", conftest.ChromeWrapper),
        ]
        for name, cls in browser_cls:
            t0 = time()
            selenium_backends[name] = cls(port)
            b[name] = time() - t0
            # pre-load numpy for the selenium instance used in benchmarks
            selenium_backends[name].load_package("numpy")
        results["selenium init"] = b
        print_entry("selenium init", b)

        # load packages
        for package_name in ["numpy"]:
            b = {"native": float("NaN")}
            for browser_name, cls in browser_cls:
                selenium = cls(port)
                try:
                    t0 = time()
                    selenium.load_package(package_name)
                    b[browser_name] = time() - t0
                finally:
                    selenium.driver.quit()
            results["load " + package_name] = b
            print_entry("load " + package_name, b)

        for name, content in get_benchmarks():
            results[name] = run_all(hostpython, selenium_backends, content)
            print_entry(name, results[name])
        for selenium in selenium_backends.values():
            selenium.driver.quit()
    return results


if __name__ == "__main__":
    results = main(Path(sys.argv[-2]).resolve())
    with open(sys.argv[-1], "w") as fp:
        json.dump(results, fp)
