Skip to content

Commit 044bdf2

Browse files
committed
Merge branch 'main' into Xmader/chore/spidermonkey-115
2 parents 8e7c4ea + 61c718c commit 044bdf2

File tree

17 files changed

+286
-122
lines changed

17 files changed

+286
-122
lines changed

.eslintrc.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,23 @@ module.exports = {
2525
},
2626
rules: {
2727
'indent': [ 'warn', 2, {
28-
SwitchCase: 1,
29-
ignoredNodes: ['CallExpression', 'ForStatement'],
30-
}
31-
],
28+
SwitchCase: 1,
29+
ignoredNodes: ['CallExpression', 'ForStatement'],
30+
}],
3231
'linebreak-style': [ 'error', 'unix' ],
33-
'quotes': [ 'warn', 'single' ],
3432
'func-call-spacing': [ 'off', 'never' ],
3533
'no-prototype-builtins': 'off',
36-
'quotes': ['warn', 'single', 'avoid-escape'],
34+
'quotes': [ 'warn', 'single', 'avoid-escape' ],
3735
'no-empty': [ 'warn' ],
3836
'no-multi-spaces': [ 'off' ],
3937
'prettier/prettier': [ 'off' ],
4038
'vars-on-top': [ 'error' ],
4139
'no-var': [ 'off' ],
4240
'spaced-comment': [ 'warn' ],
43-
'brace-style': [ 'off' ],
41+
'brace-style': [ 'warn', 'allman' ],
4442
'no-eval': [ 'error' ],
4543
'object-curly-spacing': [ 'warn', 'always' ],
4644
'eqeqeq': [ 'warn', 'always' ],
47-
'no-dupe-keys': [ 'warn' ],
4845
'no-constant-condition': [ 'warn' ],
4946
'no-extra-boolean-cast': [ 'warn' ],
5047
'no-sparse-arrays': [ 'off' ],
@@ -60,16 +57,15 @@ module.exports = {
6057
'no-unused-expressions': [ 'warn', { allowShortCircuit: true, allowTernary: true } ],
6158
'prefer-promise-reject-errors': [ 'error' ],
6259
'no-throw-literal': [ 'error' ],
63-
'semi': [ 'off', { omitLastInOneLineBlock: true }], /* does not work right with exports.X = function allmanStyle */
60+
'semi': [ 'warn' ],
6461
'semi-style': [ 'warn', 'last' ],
65-
'semi-spacing': [ 'error', {'before': false, 'after': true}],
62+
'semi-spacing': [ 'error', { 'before': false, 'after': true }],
6663
'no-extra-semi': [ 'warn' ],
6764
'no-tabs': [ 'error' ],
6865
'symbol-description': [ 'error' ],
6966
'operator-linebreak': [ 'warn', 'before' ],
7067
'new-cap': [ 'warn' ],
7168
'consistent-this': [ 'error', 'that' ],
72-
'no-use-before-define': [ 'error', { functions: false, classes: false } ],
7369
'no-shadow': [ 'error' ],
7470
'no-label-var': [ 'error' ],
7571
'radix': [ 'error' ],
@@ -89,10 +85,9 @@ module.exports = {
8985
allow: ['!!'] /* really only want to allow if(x) and if(!x) but not if(!!x) */
9086
}],
9187
'no-trailing-spaces': [ 'off', {
92-
skipBlankLines: true,
93-
ignoreComments: true
94-
}
95-
],
88+
skipBlankLines: true,
89+
ignoreComments: true
90+
}],
9691
'no-unused-vars': ['warn', {
9792
vars: 'all',
9893
args: 'none',

include/PyEventLoop.hh

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <Python.h>
1616
#include <vector>
1717
#include <utility>
18+
#include <atomic>
1819

1920
struct PyEventLoop {
2021
public:
@@ -179,9 +180,54 @@ public:
179180
*/
180181
static PyEventLoop getMainLoop();
181182

182-
protected:
183-
PyObject *_loop;
183+
struct Lock {
184+
public:
185+
explicit Lock() {
186+
PyObject *asyncio = PyImport_ImportModule("asyncio");
187+
_queueIsEmpty = PyObject_CallMethod(asyncio, "Event", NULL); // _queueIsEmpty = asyncio.Event()
188+
Py_DECREF(asyncio);
189+
190+
// The flag should initially be set as the queue is initially empty
191+
Py_XDECREF(PyObject_CallMethod(_queueIsEmpty, "set", NULL)); // _queueIsEmpty.set()
192+
};
193+
~Lock() {
194+
Py_DECREF(_queueIsEmpty);
195+
}
184196

197+
/**
198+
* @brief Increment the counter for the number of our job functions in the Python event-loop
199+
*/
200+
inline void incCounter() {
201+
_counter++;
202+
Py_XDECREF(PyObject_CallMethod(_queueIsEmpty, "clear", NULL)); // _queueIsEmpty.clear()
203+
}
204+
205+
/**
206+
* @brief Decrement the counter for the number of our job functions in the Python event-loop
207+
*/
208+
inline void decCounter() {
209+
_counter--;
210+
if (_counter == 0) { // no job queueing
211+
// Notify that the queue is empty and awake (unblock) the event-loop shield
212+
Py_XDECREF(PyObject_CallMethod(_queueIsEmpty, "set", NULL)); // _queueIsEmpty.set()
213+
} else if (_counter < 0) { // something went wrong
214+
PyErr_SetString(PyExc_RuntimeError, "Event-loop job counter went below zero.");
215+
}
216+
}
217+
218+
/**
219+
* @brief An `asyncio.Event` instance to notify that there are no queueing asynchronous jobs
220+
* @see https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event
221+
*/
222+
PyObject *_queueIsEmpty = nullptr;
223+
protected:
224+
std::atomic_int _counter = 0;
225+
};
226+
227+
static inline PyEventLoop::Lock *_locker;
228+
229+
PyObject *_loop;
230+
protected:
185231
PyEventLoop() = delete;
186232
PyEventLoop(PyObject *loop) : _loop(loop) {};
187233
private:

include/internalBinding.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
namespace InternalBinding {
1616
extern JSFunctionSpec utils[];
17+
extern JSFunctionSpec timers[];
1718
}
1819

1920
JSObject *createInternalBindingsForNamespace(JSContext *cx, JSFunctionSpec *methodSpecs);

peter-jr

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ cd "$runDir"
2121
set -u
2222
set -o pipefail
2323
peter_exit_code=2
24+
defaultTimeout="${defaultTimeout:-15}"
2425

2526
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
2627
cat <<EOF
@@ -160,13 +161,14 @@ findTests \
160161
FAIL="$(red FAIL)"
161162
fi
162163
(
164+
timeout=$(egrep '^( *[*#] *timeout: )' "$file" | head -1 | sed -e 's/^\( *[*#] *timeout: \)//')
163165
case "$testType" in
164166
"simple")
165-
"${PMJS}" "$file"
167+
eval timeout -s9 "${timeout:-${defaultTimeout}}" "${PMJS}" \"$file\"
166168
exitCode="$?"
167169
;;
168170
"bash")
169-
bash "$file"
171+
eval timeout -s9 "${timeout:-${defaultTimeout}}" bash \"$file\"
170172
exitCode="$?"
171173
;;
172174
*)

python/pythonmonkey/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
__version__= importlib.metadata.version(__name__)
99
del importlib
1010

11-
# Load the module by default to make `console`/`atob`/`btoa` globally available
11+
# Load the module by default to expose global APIs
1212
require("console")
1313
require("base64")
14+
require("timers")
1415

1516
# Add the `.keys()` method on `Object.prototype` to get JSObjectProxy dict() conversion working
1617
# Conversion from a dict-subclass to a strict dict by `dict(subclass)` internally calls the .keys() method to read the dictionary keys,

python/pythonmonkey/builtin_modules/internal-binding.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,19 @@ declare function internalBinding(namespace: "utils"): {
3030
getProxyDetails<T extends object>(proxy: T): undefined | [target: T, handler: ProxyHandler<T>];
3131
};
3232

33+
declare function internalBinding(namespace: "timers"): {
34+
/**
35+
* internal binding helper for the `setTimeout` global function
36+
*
37+
* **UNSAFE**, does not perform argument type checks
38+
* @return timeoutId
39+
*/
40+
enqueueWithDelay(handler: Function, delaySeconds: number): number;
41+
42+
/**
43+
* internal binding helper for the `clearTimeout` global function
44+
*/
45+
cancelByTimeoutId(timeoutId: number): void;
46+
};
47+
3348
export = internalBinding;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @file timers.js
3+
* @author Tom Tang <[email protected]>
4+
* @date May 2023
5+
*/
6+
const internalBinding = require('internal-binding');
7+
8+
const {
9+
enqueueWithDelay,
10+
cancelByTimeoutId
11+
} = internalBinding('timers');
12+
13+
/**
14+
* Implement the `setTimeout` global function
15+
* @see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout and
16+
* @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
17+
* @param {Function | string} handler
18+
* @param {number} delayMs timeout milliseconds, use value of 0 if this is omitted
19+
* @param {any[]} args additional arguments to be passed to the `handler`
20+
* @return {number} timeoutId
21+
*/
22+
function setTimeout(handler, delayMs = 0, ...args)
23+
{
24+
// Ensure the first parameter is a function
25+
// We support passing a `code` string to `setTimeout` as the callback function
26+
if (typeof handler !== 'function')
27+
handler = new Function(handler);
28+
29+
// `setTimeout` allows passing additional arguments to the callback, as spec-ed
30+
// FIXME (Tom Tang): the spec doesn't allow additional arguments to be passed if the original `handler` is not a function
31+
const thisArg = globalThis; // HTML spec requires `thisArg` to be the global object
32+
// Wrap the job function into a bound function with the given additional arguments
33+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
34+
/** @type {Function} */
35+
const boundHandler = handler.bind(thisArg, ...args);
36+
37+
// Get the delay time in seconds
38+
// JS `setTimeout` takes milliseconds, but Python takes seconds
39+
delayMs = Number(delayMs) || 0; // ensure the `delayMs` is a `number`, explicitly do type coercion
40+
if (delayMs < 0)
41+
delayMs = 0; // as spec-ed
42+
const delaySeconds = delayMs / 1000; // convert ms to s
43+
44+
return enqueueWithDelay(boundHandler, delaySeconds);
45+
}
46+
47+
/**
48+
* Implement the `clearTimeout` global function
49+
* @see https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout and
50+
* @see https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-cleartimeout
51+
* @param {number} timeoutId
52+
* @return {void}
53+
*/
54+
function clearTimeout(timeoutId)
55+
{
56+
return cancelByTimeoutId(timeoutId);
57+
}
58+
59+
if (!globalThis.setTimeout)
60+
globalThis.setTimeout = setTimeout;
61+
if (!globalThis.clearTimeout)
62+
globalThis.clearTimeout = clearTimeout;
63+
64+
exports.setTimeout = setTimeout;
65+
exports.clearTimeout = clearTimeout;

python/pythonmonkey/cli/pmjs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import sys, os, signal, getopt
77
import readline
8+
import asyncio
89
import pythonmonkey as pm
910
from pythonmonkey.lib import pmdb
1011

@@ -356,8 +357,11 @@ def main():
356357
assert False, "unhandled option"
357358

358359
if (len(args) > 0):
359-
globalInitModule.patchGlobalRequire()
360-
pm.runProgramModule(args[0], args, requirePath)
360+
async def runJS():
361+
globalInitModule.patchGlobalRequire()
362+
pm.runProgramModule(args[0], args, requirePath)
363+
await pm.wait() # blocks until all asynchronous calls finish
364+
asyncio.run(runJS())
361365
elif (enterRepl or forceRepl):
362366
globalInitModule.initReplLibs()
363367
repl()

python/pythonmonkey/global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ declare var console: import("console").Console;
5151
declare var atob: typeof import("base64").atob;
5252
declare var btoa: typeof import("base64").btoa;
5353

54+
// Expose `setTimeout`/`clearTimeout` APIs
55+
declare var setTimeout: typeof import("timers").setTimeout;
56+
declare var clearTimeout: typeof import("timers").clearTimeout;
57+
5458
// Keep this in sync with both https://hg.mozilla.org/releases/mozilla-esr102/file/a03fde6/js/public/Promise.h#l331
5559
// and https://github.com/nodejs/node/blob/v20.2.0/deps/v8/include/v8-promise.h#L30
5660
declare enum PromiseState { Pending = 0, Fulfilled = 1, Rejected = 2 }

python/pythonmonkey/pythonmonkey.pyi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ def eval(code: str, evalOpts: EvalOptions = {}, /) -> _typing.Any:
2222
JavaScript evaluator in Python
2323
"""
2424

25+
def wait() -> _typing.Awaitable[None]:
26+
"""
27+
Block until all asynchronous jobs (Promise/setTimeout/etc.) finish.
28+
29+
```py
30+
await pm.wait()
31+
```
32+
33+
This is the event-loop shield that protects the loop from being prematurely terminated.
34+
"""
35+
2536
def isCompilableUnit(code: str) -> bool:
2637
"""
2738
Hint if a string might be compilable Javascript without actual evaluation

0 commit comments

Comments
 (0)