Skip to content

Commit b463d3e

Browse files
authored
Merge pull request #78 from Distributive-Network/wes/better-stacks
Improve error stacks
2 parents c552bfe + df4499a commit b463d3e

File tree

6 files changed

+124
-41
lines changed

6 files changed

+124
-41
lines changed

python/pythonmonkey/cli/pmjs.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys, os, readline, signal, getopt
77
import pythonmonkey as pm
88
globalThis = pm.eval("globalThis")
9-
evalOptions = { 'strict': False }
9+
evalOpts = { 'filename': __file__, 'fromPythonFrame': True, 'strict': False }
1010

1111
if (os.getenv('PMJS_PATH')):
1212
requirePath = list(map(os.path.abspath, os.getenv('PMJS_PATH').split(':')))
@@ -40,11 +40,17 @@
4040
pythonCmd.serial = 0;
4141
return;
4242
}
43-
44-
if (arguments[0] === 'from' || arguments[0] === 'import')
43+
try {
44+
if (arguments[0] === 'from' || arguments[0] === 'import')
4545
return python.exec(cmd);
4646
47-
const retval = python.eval(cmd);
47+
const retval = python.eval(cmd);
48+
}
49+
catch(error) {
50+
globalThis._error = error;
51+
return util.inspect(error);
52+
}
53+
4854
pythonCmd.serial = (pythonCmd.serial || 0) + 1;
4955
globalThis['$' + pythonCmd.serial] = retval;
5056
python.stdout.write('$' + pythonCmd.serial + ' = ');
@@ -127,7 +133,7 @@
127133
return util.inspect(error);
128134
}
129135
}
130-
""");
136+
""", evalOpts);
131137

132138
def repl():
133139
"""
@@ -319,21 +325,20 @@ def main():
319325
print(pm.__version__)
320326
sys.exit()
321327
elif o in ("--use-strict"):
322-
evalOptions['strict'] = True
328+
evalOpts['strict'] = True
323329
elif o in ("-h", "--help"):
324330
usage()
325331
sys.exit()
326332
elif o in ("-i", "--interactive"):
327333
forceRepl = True
328334
elif o in ("-e", "--eval"):
329-
pm.eval(a, evalOptions)
335+
pm.eval(a, evalOpts)
330336
enterRepl = False
331337
elif o in ("-p", "--print"):
332-
print(pm.eval(a, evalOptions))
338+
print(pm.eval(a, evalOpts))
333339
enterRepl = False
334340
elif o in ("-r", "--require"):
335341
globalThis.require(a)
336-
# pm.eval('require')(a)
337342
else:
338343
assert False, "unhandled option"
339344

python/pythonmonkey/pythonmonkey.pyi

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@ stub file for type hints & documentations for the native module
55

66
import typing as _typing
77

8+
class EvalOptions(_typing.TypedDict, total=False):
9+
filename: str
10+
lineno: int
11+
column: int
12+
mutedErrors: bool
13+
noScriptRval: bool
14+
selfHosting: bool
15+
strict: bool
16+
module: bool
17+
fromPythonFrame: bool
18+
819
# pylint: disable=redefined-builtin
9-
def eval(code: str, /) -> _typing.Any:
20+
def eval(code: str, evalOpts: EvalOptions = {}, /) -> _typing.Any:
1021
"""
1122
JavaScript evaluator in Python
1223
"""

python/pythonmonkey/require.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@
4040
"node_modules"
4141
)
4242
)
43+
evalOpts = { 'filename': __file__, 'fromPythonFrame': True }
4344

4445
# Add some python functions to the global python object for code in this file to use.
45-
globalThis = pm.eval("globalThis;")
46-
pm.eval("globalThis.python = { pythonMonkey: {}, stdout: {}, stderr: {} }")
46+
globalThis = pm.eval("globalThis;", evalOpts)
47+
pm.eval("globalThis.python = { pythonMonkey: {}, stdout: {}, stderr: {} }", evalOpts);
4748
globalThis.pmEval = pm.eval
4849
globalThis.python.pythonMonkey.dir = os.path.dirname(__file__)
4950
#globalThis.python.pythonMonkey.version = pm.__version__
@@ -59,15 +60,15 @@
5960
globalThis.python.exec = exec
6061
globalThis.python.getenv = os.getenv
6162
globalThis.python.paths = ':'.join(sys.path)
62-
pm.eval("python.paths = python.paths.split(':');"); # fix when pm supports arrays
63+
pm.eval("python.paths = python.paths.split(':');", evalOpts); # fix when pm supports arrays
6364

6465
globalThis.python.exit = pm.eval("""'use strict';
6566
(exit) => function pythonExitWrapper(exitCode) {
6667
if (typeof exitCode === 'number')
6768
exitCode = BigInt(Math.floor(exitCode));
6869
exit(exitCode);
6970
}
70-
""")(sys.exit);
71+
""", evalOpts)(sys.exit);
7172

7273
# bootstrap is effectively a scoping object which keeps us from polluting the global JS scope.
7374
# The idea is that we hold a reference to the bootstrap object in Python-load, for use by the
@@ -78,7 +79,7 @@
7879
7980
const bootstrap = {
8081
modules: {
81-
vm: { runInContext: eval },
82+
vm: {},
8283
'ctx-module': {},
8384
},
8485
}
@@ -92,7 +93,7 @@
9293
throw new Error('module not found: ' + mid);
9394
}
9495
95-
bootstrap.modules.vm.runInContext_broken = function runInContext(code, _unused_contextifiedObject, options)
96+
bootstrap.modules.vm.runInContext = function runInContext(code, _unused_contextifiedObject, options)
9697
{
9798
var evalOptions = {};
9899
@@ -169,7 +170,7 @@
169170
globalThis.bootstrap = bootstrap;
170171
171172
return bootstrap;
172-
})(globalThis.python)""")
173+
})(globalThis.python)""", evalOpts)
173174

174175
def statSync_inner(filename: str) -> Union[Dict[str, int], bool]:
175176
"""
@@ -208,15 +209,18 @@ def existsSync(filename: str) -> bool:
208209
# require and exports symbols injected from the bootstrap object above. Current PythonMonkey bugs
209210
# prevent us from injecting names properly so they are stolen from trail left behind in the global
210211
# scope until that can be fixed.
212+
#
213+
# lineno should be -5 but jsapi 102 uses unsigned line numbers, so we take the newlines out of the
214+
# wrapper prologue to make stack traces line up.
211215
with open(node_modules + "/ctx-module/ctx-module.js", "r") as ctxModuleSource:
212216
initCtxModule = pm.eval("""'use strict';
213217
(function moduleWrapper_forCtxModule(broken_require, broken_exports)
214218
{
215219
const require = bootstrap.require;
216220
const exports = bootstrap.modules['ctx-module'];
217-
""" + ctxModuleSource.read() + """
221+
""".replace("\n", " ") + "\n" + ctxModuleSource.read() + """
218222
})
219-
""");
223+
""", { 'filename': node_modules + "/ctx-module/ctx-module.js", 'lineno': 0 });
220224
#broken initCtxModule(bootstrap.require, bootstrap.modules['ctx-module'].exports)
221225
initCtxModule();
222226

@@ -298,7 +302,7 @@ def _createRequireInner(*args):
298302
module.require.path.splice(module.require.path.length, 0, ...(extraPaths.split(':')));
299303
300304
return module.require;
301-
})""")(*args)
305+
})""", evalOpts)(*args)
302306

303307
def createRequire(filename, extraPaths: Union[List[str], Literal[False]] = False, isMain = False):
304308
"""
@@ -331,7 +335,7 @@ def runProgramModule(filename, argv, extraPaths=[]):
331335
globalThis.__filename = fullFilename;
332336
globalThis.__dirname = os.path.dirname(fullFilename);
333337
with open(fullFilename, encoding="utf-8", mode="r") as mainModuleSource:
334-
pm.eval(mainModuleSource.read())
338+
pm.eval(mainModuleSource.read(), {'filename': fullFilename})
335339

336340
def require(moduleIdentifier: str):
337341
# Retrieve the caller’s filename from the call stack

src/modules/pythonmonkey/pythonmonkey.cc

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* @version 0.1
66
* @date 2023-03-29
77
*
8-
* @copyright Copyright (c) 2023
8+
* @copyright Copyright (c) 2023 Distributive Corp.
99
*
1010
*/
1111

@@ -188,7 +188,7 @@ static PyObject *eval(PyObject *self, PyObject *args) {
188188

189189
JSAutoRealm ar(GLOBAL_CX, *global);
190190
JS::CompileOptions options (GLOBAL_CX);
191-
options.setFileAndLine("@evaluate", 1)
191+
options.setFileAndLine("evaluate", 1)
192192
.setIsRunOnce(true)
193193
.setNoScriptRval(false)
194194
.setIntroductionType("pythonmonkey eval");
@@ -206,9 +206,29 @@ static PyObject *eval(PyObject *self, PyObject *args) {
206206
if (getEvalOption(evalOptions, "selfHosting", &b)) options.setSelfHostingMode(b);
207207
if (getEvalOption(evalOptions, "strict", &b)) if (b) options.setForceStrictMode();
208208
if (getEvalOption(evalOptions, "module", &b)) if (b) options.setModule();
209-
}
210209

211-
// initialize JS context
210+
if (getEvalOption(evalOptions, "fromPythonFrame", &b) && b) {
211+
#if PY_VERSION_HEX >= 0x03090000
212+
PyFrameObject *frame = PyEval_GetFrame();
213+
if (frame && !getEvalOption(evalOptions, "lineno", &l)) {
214+
options.setLine(PyFrame_GetLineNumber(frame));
215+
} /* lineno */
216+
#endif
217+
#if 0 && (PY_VERSION_HEX >= 0x030a0000) && (PY_VERSION_HEX < 0x030c0000)
218+
PyObject *filename = PyDict_GetItemString(frame->f_builtins, "__file__");
219+
#elif (PY_VERSION_HEX >= 0x030c0000)
220+
PyObject *filename = PyDict_GetItemString(PyFrame_GetGlobals(frame), "__file__");
221+
#else
222+
PyObject *filename = NULL;
223+
#endif
224+
if (!getEvalOption(evalOptions, "filename", &s)) {
225+
if (filename && PyUnicode_Check(filename)) {
226+
options.setFile(PyUnicode_AsUTF8(filename));
227+
}
228+
} /* filename */
229+
} /* fromPythonFrame */
230+
} /* eval options */
231+
// initialize JS context
212232
JS::SourceText<mozilla::Utf8Unit> source;
213233
if (!source.init(GLOBAL_CX, code->getValue(), strlen(code->getValue()), JS::SourceOwnership::Borrowed)) {
214234
setSpiderMonkeyException(GLOBAL_CX);
@@ -230,13 +250,13 @@ static PyObject *eval(PyObject *self, PyObject *args) {
230250
}
231251

232252
// TODO: Find a better way to destroy the root when necessary (when the returned Python object is GCed).
233-
js::ESClass cls = js::ESClass::Other; // placeholder if `rval` is not a JSObject
253+
js::ESClass cls = js::ESClass::Other; // placeholder if `rval` is not a JSObject
234254
if (rval->isObject()) {
235255
JS::GetBuiltinClass(GLOBAL_CX, JS::RootedObject(GLOBAL_CX, &rval->toObject()), &cls);
236256
}
237-
bool rvalIsFunction = cls == js::ESClass::Function; // function object
238-
bool rvalIsString = rval->isString() || cls == js::ESClass::String; // string primitive or boxed String object
239-
if (!(rvalIsFunction || rvalIsString)) { // rval may be a JS function or string which must be kept alive.
257+
bool rvalIsFunction = cls == js::ESClass::Function; // function object
258+
bool rvalIsString = rval->isString() || cls == js::ESClass::String; // string primitive or boxed String object
259+
if (!(rvalIsFunction || rvalIsString)) { // rval may be a JS function or string which must be kept alive.
240260
delete rval;
241261
}
242262

@@ -278,7 +298,7 @@ struct PyModuleDef pythonmonkey =
278298
{
279299
PyModuleDef_HEAD_INIT,
280300
"pythonmonkey", /* name of module */
281-
"A module for python to JS interoperability", /* module documentation, may be NULL */
301+
"A module for python to JS interoperability", /* module documentation, may be NULL */
282302
-1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */
283303
PythonMonkeyMethods
284304
};
@@ -302,29 +322,29 @@ static bool setTimeout(JSContext *cx, unsigned argc, JS::Value *vp) {
302322

303323
// Get the function to be executed
304324
// FIXME (Tom Tang): memory leak, not free-ed
305-
JS::RootedObject *thisv = new JS::RootedObject(cx, JS::GetNonCCWObjectGlobal(&args.callee())); // HTML spec requires `thisArg` to be the global object
325+
JS::RootedObject *thisv = new JS::RootedObject(cx, JS::GetNonCCWObjectGlobal(&args.callee())); // HTML spec requires `thisArg` to be the global object
306326
JS::RootedValue *jobArg = new JS::RootedValue(cx, jobArgVal);
307327
// `setTimeout` allows passing additional arguments to the callback, as spec-ed
308-
if (args.length() > 2) { // having additional arguments
328+
if (args.length() > 2) { // having additional arguments
309329
// Wrap the job function into a bound function with the given additional arguments
310330
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
311331
JS::RootedVector<JS::Value> bindArgs(cx);
312-
(void)bindArgs.append(JS::ObjectValue(**thisv)); /** @todo XXXwg handle return value */
332+
(void)bindArgs.append(JS::ObjectValue(**thisv)); /** @todo XXXwg handle return value */
313333
for (size_t j = 2; j < args.length(); j++) {
314-
(void)bindArgs.append(args[j]); /** @todo XXXwg handle return value */
334+
(void)bindArgs.append(args[j]); /** @todo XXXwg handle return value */
315335
}
316336
JS::RootedObject jobArgObj = JS::RootedObject(cx, &jobArgVal.toObject());
317-
JS_CallFunctionName(cx, jobArgObj, "bind", JS::HandleValueArray(bindArgs), jobArg); // jobArg = jobArg.bind(thisv, ...bindArgs)
337+
JS_CallFunctionName(cx, jobArgObj, "bind", JS::HandleValueArray(bindArgs), jobArg); // jobArg = jobArg.bind(thisv, ...bindArgs)
318338
}
319339
// Convert to a Python function
320340
PyObject *job = pyTypeFactory(cx, thisv, jobArg)->getPyObject();
321341

322342
// Get the delay time
323343
// JS `setTimeout` takes milliseconds, but Python takes seconds
324-
double delayMs = 0; // use value of 0 if the delay parameter is omitted
325-
if (args.hasDefined(1)) { JS::ToNumber(cx, args[1], &delayMs); } // implicitly do type coercion to a `number`
326-
if (delayMs < 0) { delayMs = 0; } // as spec-ed
327-
double delaySeconds = delayMs / 1000; // convert ms to s
344+
double delayMs = 0; // use value of 0 if the delay parameter is omitted
345+
if (args.hasDefined(1)) { JS::ToNumber(cx, args[1], &delayMs); } // implicitly do type coercion to a `number`
346+
if (delayMs < 0) { delayMs = 0; } // as spec-ed
347+
double delaySeconds = delayMs / 1000; // convert ms to s
328348

329349
// Schedule job to the running Python event-loop
330350
PyEventLoop loop = PyEventLoop::getRunningLoop();
@@ -355,7 +375,7 @@ static bool clearTimeout(JSContext *cx, unsigned argc, JS::Value *vp) {
355375
// Retrieve the AsyncHandle by `timeoutID`
356376
int32_t timeoutID = timeoutIdArg.toInt32();
357377
AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID);
358-
if (!handle) return true; // does nothing on invalid timeoutID
378+
if (!handle) return true; // does nothing on invalid timeoutID
359379

360380
// Cancel this job on Python event-loop
361381
handle->cancel();
@@ -424,7 +444,7 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void)
424444
// In https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/js/src/jit/CacheIR.cpp#l317, trying to use the callback returned by `js::GetDOMProxyShadowsCheck()` even it's unset (nullptr)
425445
// Temporarily solved by explicitly setting the `domProxyShadowsCheck` callback here
426446
JS::SetDOMProxyInformation(nullptr,
427-
[](JSContext *, JS::HandleObject, JS::HandleId) { // domProxyShadowsCheck
447+
[](JSContext *, JS::HandleObject, JS::HandleId) { // domProxyShadowsCheck
428448
return JS::DOMProxyShadowsResult::ShadowCheckFailed;
429449
}, nullptr);
430450

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#! /bin/bash
2+
#
3+
# @file require-module-stack.bash
4+
# A peter-jr test which tests that requiring a module yields JS stacks with the correct
5+
# filename and line number information.
6+
#
7+
# NOTE: This test currently fails because the stack coming back through python has
8+
# underscores instead of slashes in the filename
9+
#
10+
# @author Wes Garland, [email protected]
11+
# @date July 2023
12+
13+
set -u
14+
set -o pipefail
15+
16+
panic()
17+
{
18+
echo "FAIL: $*" >&2
19+
exit 2
20+
}
21+
22+
cd `dirname "$0"` || panic "could not change to test directory"
23+
24+
if "${PMJS:-../../pmjs}" -r ./throw-filename ./program.js \
25+
| grep '^@/home/wes/git/pythonmonkey2/tests/js/throw-filename.js:9:9$'; then
26+
echo 'pass'
27+
fi
28+
29+
panic "did not find correct stack info"

tests/js/throw-filename.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @file throw-filename.js
3+
* A helper that throws when loaded, so that we can test that loading things throws with the right filename.
4+
* @author Wes Garland, [email protected]
5+
* @date July 2023
6+
*/
7+
try
8+
{
9+
throw new Error("lp0 on fire");
10+
}
11+
catch(error)
12+
{
13+
console.log(error.stack)
14+
}

0 commit comments

Comments
 (0)