Skip to content

Commit 190a1a2

Browse files
authored
Merge pull request #83 from Distributive-Network/wes/pmjs-improved-parsing
pmjs improved parsing
2 parents b9fb965 + 998fef4 commit 190a1a2

File tree

5 files changed

+138
-29
lines changed

5 files changed

+138
-29
lines changed

README.md

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@
33
![Testing Suite](https://github.com/Kings-Distributed-Systems/PythonMonkey/actions/workflows/tests.yaml/badge.svg)
44

55
## About
6-
PythonMonkey is a Mozilla [SpiderMonkey](https://firefox-source-docs.mozilla.org/js/index.html) JavaScript engine embedded into the Python VM,
6+
[PythonMonkey](https://pythonmonkey.io) is a Mozilla [SpiderMonkey](https://firefox-source-docs.mozilla.org/js/index.html) JavaScript engine embedded into the Python VM,
77
using the Python engine to provide the JS host environment.
88

9-
This product is in an early stage, approximately 80% to MVP as of July 2023. It is under active development by Distributive Corp.,
10-
https://distributive.network/. External contributions and feedback are welcome and encouraged.
9+
This product is in an early stage, approximately 80% to MVP as of July 2023. It is under active development by [Distributive](https://distributive.network/).
10+
External contributions and feedback are welcome and encouraged.
1111

12-
The goal is to make writing code in either JS or Python a developer preference, with libraries commonly used in either language
13-
available eveywhere, with no significant data exchange or transformation penalties. For example, it should be possible to use NumPy
14-
methods from a JS library, or to refactor a slow "hot loop" written in Python to execute in JS instead, taking advantage of
15-
SpiderMonkey's JIT for near-native speed, rather than writing a C-language module for Python. At Distributive, we intend to use
16-
this package to execute our complex `dcp-client` library, which is written in JS and enables distributed computing on the web stack.
12+
### tl;dr
13+
```bash
14+
$ pip install pythonmonkey
15+
```
16+
```python
17+
from pythonmonkey import eval as js_eval
18+
19+
js_eval("console.log")('hello, world')
20+
```
21+
22+
### Goals
23+
- **Fast** and memory-efficient
24+
- Make writing code in either JS or Python a developer preference
25+
- Use JavaScript libraries from Python
26+
- Use Python libraries from JavaScript
27+
- Same process runs both JS and Python VMs - no serialization, pipes, etc
1728

1829
### Data Interchange
1930
- Strings share immutable backing stores whenever possible (when allocating engine choses UCS-2 or Latin-1 internal string representation) to keep memory consumption under control, and to make it possible to move very large strings between JS and Python library code without memory-copy overhead.
@@ -243,10 +254,71 @@ globalThis.python.exit = pm.eval("""'use strict';
243254
""")(sys.exit);
244255
```
245256

246-
# Troubleshooting Tips
257+
# pmjs
258+
A basic JavaScript shell, `pmjs`, ships with PythonMonkey. This shell can act as a REPL or run
259+
JavaScript programs; it is conceptually similar to the `node` shell which ships with Node.js.
260+
261+
## Modules
262+
Pmjs starts PythonMonkey's CommonJS subsystem, which allow it to use CommonJS modules, with semantics
263+
that are similar to Node.js - e.g. searching module.paths, understanding package.json, index.js, and
264+
so on. See the [ctx-module](https://www.npmjs.com/package/ctx-module) for a full list of supported
265+
features.
266+
267+
In addition to CommonJS modules written in JavaScript, PythonMonkey supports CommonJS modules written
268+
in Python. Simply decorate a Dict named `exports` inside a file with a `.py` extension, and it can be
269+
loaded by `require()` -- in either JavaScript or Python.
270+
271+
### Program Module
272+
The program module, or main module, is a special module in CommonJS. In a program module,
273+
- variables defined in the outermost scope are properties of `globalThis`
274+
- returning from the outermost scope is a syntax error
275+
- the `arguments` variable in an Array-like object which holds your program's argument vector
276+
(command-line arguments)
277+
278+
```console
279+
# echo "console.log('hello world')" > my-program.js
280+
# pmjs my-program.js
281+
hello world
282+
#
283+
```
247284

248-
## REPL - pmjs
249-
A basic JavaScript shell, `pmjs`, ships with PythonMonkey. This shell can also run JavaScript programs with
285+
### CommonJS Module: JavaScript language
286+
```python
287+
# date-lib.js - require("./date-lib")
288+
const d = new Date();
289+
exports.today = `${d.getFullYear()}-${String(d.getMonth()).padStart(2,'0')}-${String(d.getDay()).padStart(2,'0')}`
290+
```
291+
292+
### CommonJS Module: Python language
293+
```python
294+
# date-lib.py - require("./date-lib")
295+
from datetime import date # You can use Python libraries.
296+
exports['today'] = date.today()
297+
```
298+
299+
# Troubleshooting Tips
250300

251301
## CommonJS (require)
252302
If you are having trouble with the CommonJS require function, set environment variable DEBUG='ctx-module*' and you can see the filenames it tries to laod.
303+
304+
## pmjs
305+
- there is a `.help` menu in the REPL
306+
- there is a `--help` command-line option
307+
- the `-r` option can be used to load a module before your program or the REPL runs
308+
- the `-e` option can be used evaluate code -- e.g. define global variables -- before your program or the REPL runs
309+
- The REPL can evaluate Python expressions, storing them in variables named `$1`, `$2`, etc. ```
310+
# ./pmjs
311+
Welcome to PythonMonkey v0.2.0.
312+
Type ".help" for more information.
313+
> .python import sys
314+
> .python sys.path
315+
$1 = { '0': '/home/wes/git/pythonmonkey2',
316+
'1': '/usr/lib/python310.zip',
317+
'2': '/usr/lib/python3.10',
318+
'3': '/usr/lib/python3.10/lib-dynload',
319+
'4': '/home/wes/.cache/pypoetry/virtualenvs/pythonmonkey-StuBmUri-py3.10/lib/python3.10/site-packages',
320+
'5': '/home/wes/git/pythonmonkey2/python' }
321+
> $1[3]
322+
'/usr/lib/python3.10/lib-dynload'
323+
>
324+
```

pmjs

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

1011
if (os.getenv('PMJS_PATH')):
1112
requirePath = list(map(os.path.abspath, os.getenv('PMJS_PATH').split(':')))
1213
else:
1314
requirePath = False;
1415

15-
globalThis = pm.eval("globalThis;")
16-
1716
pm.eval("""'use strict';
1817
const cmds = {};
1918
@@ -24,7 +23,7 @@ cmds.help = function help() {
2423
.load Load JS from a file into the REPL session
2524
.save Save all evaluated commands in this REPL session to a file
2625
.python Evaluate a Python statement, returning result as global variable $n.
27-
Use '.python reset' to rest back to $1.
26+
Use '.python reset' to reset back to $1.
2827
Statement starting with 'from' or 'import' are silently executed.
2928
3029
Press Ctrl+C to abort current expression, Ctrl+D to exit the REPL`
@@ -36,6 +35,8 @@ cmds.python = function pythonCmd(...args) {
3635
3736
if (cmd === 'reset')
3837
{
38+
for (let i=0; i < pythonCmd.serial; i++)
39+
delete globalThis['$' + i];
3940
pythonCmd.serial = 0;
4041
return;
4142
}
@@ -45,8 +46,9 @@ cmds.python = function pythonCmd(...args) {
4546
4647
const retval = python.eval(cmd);
4748
pythonCmd.serial = (pythonCmd.serial || 0) + 1;
48-
python.print('$' + pythonCmd.serial, '=', util.inspect(retval));
4949
globalThis['$' + pythonCmd.serial] = retval;
50+
python.stdout.write('$' + pythonCmd.serial + ' = ');
51+
return util.inspect(retval);
5052
};
5153
5254
/**
@@ -83,13 +85,45 @@ globalThis.replCmd = function replCmd(cmdLine)
8385
globalThis.replEval = function replEval(statement)
8486
{
8587
const indirectEval = eval;
88+
var originalStatement = statement;
89+
var result;
90+
var mightBeObjectLiteral = false;
91+
92+
/* A statement which starts with a { and does not end with a ; is treated as an object literal,
93+
* and to get the parser in to Expression mode instead of Statement mode, we surround any expression
94+
* like that which is also a valid compilation unit with parens, then if that is a syntax error,
95+
* we re-evaluate without the parens.
96+
*/
97+
if (/^\\s*\{.*[^;\\s]\\s*$/.test(statement))
98+
{
99+
const testStatement = `(${statement})`;
100+
if (globalThis.python.pythonMonkey.isCompilableUnit(testStatement))
101+
statement = testStatement;
102+
}
103+
86104
try
87105
{
88-
const result = indirectEval(`${statement}`);
106+
try
107+
{
108+
result = indirectEval(statement);
109+
}
110+
catch(evalError)
111+
{
112+
/* Don't retry paren-wrapped statements which weren't syntax errors, as they might have
113+
* side-effects. Never retry if we didn't paren-wrap.
114+
*/
115+
if (!(evalError instanceof SyntaxError) || originalStatement === statement)
116+
throw evalError;
117+
globalThis._swallowed_error = evalError;
118+
result = indirectEval(originalStatement);
119+
}
120+
121+
globalThis._ = result;
89122
return util.inspect(result);
90123
}
91124
catch(error)
92125
{
126+
globalThis._error = error;
93127
return util.inspect(error);
94128
}
95129
}
@@ -193,7 +227,9 @@ def repl():
193227
if (len(statement) == 0):
194228
continue
195229
if (statement[0] == '.'):
196-
print(globalThis.replCmd(statement[1:]))
230+
cmd_output = globalThis.replCmd(statement[1:]);
231+
if (cmd_output != None):
232+
print(cmd_output)
197233
statement = ""
198234
continue
199235
if (pm.isCompilableUnit(statement)):
@@ -236,6 +272,7 @@ Options:
236272
-p, --print [...] evaluate script and print result
237273
-r, --require... module to preload (option can be repeated)
238274
-v, --version print PythonMonkey version
275+
--use-strict evaluate -e, -p, and REPL code in strict mode
239276
240277
Environment variables:
241278
TZ specify the timezone configuration
@@ -269,7 +306,7 @@ def main():
269306
global requirePath
270307

271308
try:
272-
opts, args = getopt.getopt(sys.argv[1:], "hie:p:r:v", ["help", "eval=", "print=", "require=", "version"])
309+
opts, args = getopt.getopt(sys.argv[1:], "hie:p:r:v", ["help", "eval=", "print=", "require=", "version", "use-strict"])
273310
except getopt.GetoptError as err:
274311
# print help information and exit:
275312
print(err) # will print something like "option -a not recognized"
@@ -281,16 +318,18 @@ def main():
281318
if o in ("-v", "--version"):
282319
print(pm.__version__)
283320
sys.exit()
321+
elif o in ("--use-strict"):
322+
evalOptions['strict'] = True
284323
elif o in ("-h", "--help"):
285324
usage()
286325
sys.exit()
287326
elif o in ("-i", "--interactive"):
288327
forceRepl = True
289328
elif o in ("-e", "--eval"):
290-
pm.eval(a)
329+
pm.eval(a, evalOptions)
291330
enterRepl = False
292331
elif o in ("-p", "--print"):
293-
print(pm.eval(a))
332+
print(pm.eval(a, evalOptions))
294333
enterRepl = False
295334
elif o in ("-r", "--require"):
296335
globalThis.require(a)

python/pythonmonkey/require.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
globalThis.python.pythonMonkey.dir = os.path.dirname(__file__)
4949
#globalThis.python.pythonMonkey.version = pm.__version__
5050
#globalThis.python.pythonMonkey.module = pm
51+
globalThis.python.pythonMonkey.isCompilableUnit = pm.isCompilableUnit;
5152
globalThis.python.print = print
5253
globalThis.python.stdout.write = sys.stdout.write
5354
globalThis.python.stderr.write = sys.stderr.write

src/modules/pythonmonkey/pythonmonkey.cc

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,20 +174,17 @@ static bool getEvalOption(PyObject *evalOptions, const char *optionName, bool *b
174174
PyObject *value;
175175

176176
value = PyDict_GetItemString(evalOptions, optionName);
177-
if (value) {
178-
if (PyLong_Check(value))
179-
*b_p = PyBool_FromLong(PyLong_AsLong(value));
180-
else
181-
*b_p = value != Py_False;
182-
}
177+
if (value)
178+
*b_p = PyObject_IsTrue(value) == 1 ? true : false;
183179
return value != NULL;
184180
}
185181

186182
static PyObject *eval(PyObject *self, PyObject *args) {
183+
size_t argc = PyTuple_GET_SIZE(args);
187184
StrType *code = new StrType(PyTuple_GetItem(args, 0));
188-
PyObject *evalOptions = PyTuple_GET_SIZE(args) == 2 ? PyTuple_GetItem(args, 1) : NULL;
185+
PyObject *evalOptions = argc == 2 ? PyTuple_GetItem(args, 1) : NULL;
189186

190-
if (!PyUnicode_Check(code->getPyObject())) {
187+
if (argc == 0 || !PyUnicode_Check(code->getPyObject())) {
191188
PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a string as its first argument");
192189
return NULL;
193190
}

tests/js/pmjs-eopt.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ do
2727
exit 111
2828
;;
2929
*)
30-
echo "Ignored: ${keyword} ${rest} (${loaded})"
30+
echo "Ignored: ${keyword} ${rest}"
3131
;;
3232
esac
3333
done

0 commit comments

Comments
 (0)