Jispy is an interpreter for a strict subset of JavaScript, fondly called LittleJ (LJ). It employs recursive descent for parsing and is very easily extendable.
Jispy's original vision was to seamlessly allow embedding JavaScript programs in Python projects. By default, it doesn't expose the host's file system or any other sensitive element. Some checks on infinite looping and infinite recursion are provided to tackle possibly malicious code.
The project is currently maintined by the engineering team at Polydojo. See LICENSE.txt and DEDICATION.md for project license and dedication respectively.
Simply include jispy.py in your project directory. For convenience, you may wish to also include stdlib.l.js, which provides multiple utilities and manipulators.
Jispy comes armed with a console and an API. While the API provides far greater control, the console is great for getting started.
You could start the console in two ways:
- Run
jispy.py
in a terminal:
$ python jispy.py
- From Python's interactive interpreter:
>>> import jispy
>>> jispy.console()
In either case, you'll unleash a LittleJ REPL (Read-Evaluate-Print Loop).
LJ> "Hello World!";
"Hello World!"
LJ> var obj = {i: 'am', an: 'object'}, arr = ['i', 'am', 'an', 'array'];
LJ> obj.i === arr[1];
true
LJ> type(obj) === type(arr);
false
LJ> "Because type() of an array is " + type(arr);
"Because type() of an array is array"
LJ> // A function to compute factorial:
LJ> var f = function (n) { if (n <= 1) { return 1; } return n * f(n-1); };
LJ> f(6);
720
LJ> // Functions are first class!!
LJ> var foo = function () { return foo; };
LJ> foo === foo() && foo === foo()();
true
LJ> // Hit Crtl-D to exit
LJ>
To enter multiple lines of code, end lines with tabs.
Here's an example with for
:
LJ> var obj = {a: 'apple', b: 'ball', c: 'cat', d: 'dog'}, // tab..
... i = null, keyz = keys(obj);
LJ> for (i = 0; i < len(obj); i += 1) { // tab..
... keyz[i] + ' for ' + obj[keyz[i]]; // tab..
... }
"a for apple"
"c for cat"
"b for ball"
"d for dog"
LJ>
Note: Future versions of LittleJ (and hence Jispy) may no longer support for
loops in favor of while
loops and _.each(.)
style iteration. If you'd like to maintain backward compatability, we recommend using array.each
from stdlib.l.js
.
The above example uses the inbuilt len()
and keys()
functions. These, along with other inbuilts have been described in LittleJ's specification.
Consider the following LittleJ program for checking if a number is prime:
var isPrime = function (n) {
var countF = 0, i = null; // countF counts the number of factors
for (i = 1; i <= n; i += 1) { if (n % i === 0) { countF += 1; } }
return countF === 2;
};
print(isPrime(7)); // true
print(isPrime(8)); // false
If the above program has been stored as a Python-string in the variable lj_isPrime
, the following python code would execute it.
# ... set lj_isPrime ...
from jispy import Runtime
rt = Runtime()
rt.runX(lj_isPrime);
A Runtime
provides a wrapper around a single global environment or scope. It allows you to run multiple programs in the same environment; at the same time allowing you to run a programs in foreign environments.
Runtime
's constructor optionally accepts three arguments (in order):
maxLoopTime
: The maximum time in seconds for which a loop may run.maxDepth
: The maximum allowable depth of nested environments (scopes).writer
:write
method of a file (opened for writing). It defaults tosys.stdout.write
.
maxLoopTime
and maxDepth
both default to None
. Unless set to a positive value, there shall be no checks on infinite looping and/or infinite recursion. If writer
is set to None
, the inbuilt print()
shall not be made available.
Here's an example involving maxLoopTime
and maxDepth
:
>>> from jispy import Runtime
>>> prog1 = """var i = 1; while (true) { i += 0; }"""
>>> prog2 = """var foo = function () { return foo(); }; foo();"""
>>> rt = Runtime(maxLoopTime=13, maxDepth=100);
>>> rt.runX(prog1);
RuntimeError: looping for to long
>>> rt.runX(prog2);
RuntimeError: maximum call depth exceeded
>>>
An instance of Runtime
(say rt
) provides 3 ways in which you may run a program. There's a method corresponding to each:
rt.runG()
runs programs inrt
's global environment.rt.runC()
runs programs in a first-level child ofrt
's global environment.rt.runX()
runs programs in a completely different (foreign) environment.
In LittleJ, once a variable is defined, it may not be redefined in the same environment. Thus, the method used to run a program is significant.
Additionally, there's an rt.run()
method, which accepts the environment in which a program should be run. Each of the above listed method internally calls rt.run()
. However, calling rt.run()
explicitly is not advised.
Each of these methods accept at least 1 and at most 2 inputs (in order):
prog
: A LittleJ program. It must be supplied.console
: A boolean. (Optional, defaults toFalse
.)
prog
may be any one of:
- A Python string in which a LittleJ program is stored. (Like
lj_isPrime
above.) - The name of an LittleJ program file. LittleJ program files have the
.l.js
extension. Any string ending in.l.js
is treated as a file name. - A parse tree generated by Jispy's (
lex()
and)yacc()
. This option is explored in later sections.
Consider:
>>> import jispy
>>> rt = jispy.Runtime();
>>> mappy = """
... var map = function (arr, func) {
... var ans = [], i = null;
... for (i = 0; i < len(arr); i += 1) {
... append(ans, func(arr[i]));
... }
... return ans;
... };"""
>>> rt.runG(mappy);
>>> rt.runG('print(map([1, 2, 3], function (x) { return x*x; }));');
[1, 4, 9]
>>> rt.runC('print(map([1, 2, 3], function (x) { return x/2; }));');
[0.5, 1, 1.5]
>>> rt.runX('print(map([1, 2, 3], function (x) { return x+2; }));');
ReferenceError: map is not defined
>>>
In the above example, since mappy
is run in rt
's global environment, the variable map
it is available to not only the global environment but also all child environments. However, it is not available in any foreign environment.
stdlib.l.js
contains many useful string, array and object related utilities. To use them in your programs, run stdlib.l.js
using runG()
Each call to Jispy's console()
uses a single Runtime
. Each input line (or lines) is executed as an independent program in the global environment (using the runG()
method). Thus, each line has access to variables defined in earlier an earlier line.
The console accepts three optional arguments (in order):
rt
: TheRuntime
in which each line (or lines) should be run. Defaults toNone
, in which case a freshRuntime
is created.semify
: A boolean, defaults toFalse
. IfTrue
(or truthy), the console tries to add semicolons to the end of lines as required.prompt
: The input prompt. Defaults to"LJ> "
Originally, the console()
was not a REPL. It now is. (But the name has stuck.)
>>> import jispy
>>> rt = jispy.Runtime(13, 100);
>>> rt.runG('stdlib.l.js');
>>> jispy.console(rt);
stdlib.l.js
includes array.map()
and array.filter()
. As the library was run in the global environment, we may freely use those functions in our LittleJ programs:
LJ> var a = [5, 8, 3, 4, 6, 7, 1];
LJ> array.map(array.filter(a, function (x) { return x % 2 === 0; }),
... function (x) { return x * x * x; });
[512, 64, 216]
LJ> // [8^3, 4^3, 6^3]
LJ>
Functions such as print()
may be added via Runtime
's addNatives
method.
It accepts a single dictionary as input. The key-value pairs are added to the global environment.
For example, let's add a native version of the factorial
function and set native inbuilt_num
to 7.0
:
>>> from jispy import Runtime
>>> factorial = lambda n: 1.0 if n <= 1.0 else n * factorial(n-1)
>>> rt = Runtime(maxLoopTime = 13, maxDepth = 100)
>>> rt.addNatives({'factorial' : factorial, 'inbuilt_num' : 7.0})
>>> demoProgo = 'print(factorial(inbuilt_num));'
>>> rt.runC(demoProgo)
5040
>>>
Please note that the LittleJ program may reassign all natives; and a native variable may be initialized (as a fresh variable) in a new environment.
>>> import jispy as j
>>> rt = j.Runtime();
>>> rt.addNatives({'input': lambda prompt: raw_input(prompt)});
>>> j.console(rt); # use input() on Python 3+
LJ> var name = input('Please enter your name: ');
Please enter your name: Awesome Person
LJ> print('Dear ' + name + ', THANK YOU FOR READING THIS FAR!');
Dear Awesome Person, THANK YOU FOR READING THIS FAR!
LJ>
The &&
and ||
, operators behave like the following functions:
var andand = function (a, b) { if (a) { return b; } return a; },
oror = function (a, b) { if (a) { return a; } return b; };
That is, both operands to these operators are ALWAYS evaluated.
Let's say we want to write a function pSquare
which squares only positive numbers. If a non-positive number is passed as input, 'bad input'
should be returned.
Here's an instinctive version of pSquare
:
LJ> var pSquare = function (n) {
... if (type(n) === 'number' && n > 0) { return n * n; }
... return 'bad input';
... };
LJ> pSquare('what?');
TypeError: bad operands for binary > ... "string" === "number" && "what?" > 0
LJ>
With n
as 'what?'
, since type('what?') === 'number'
is false
, the condition 'what?' > 0
should not be evaluated. But Jispy (incorrectly) tries to evaluate it and raises a TypeError
in the process.
The workaround is to use conditionals:
LJ> var pSquare = function (n) {
... if (type(n) === 'number') { if (n > 0) { return n * n; } }
... return 'bad input';
... };
LJ> pSquare('what?');
"bad input"
LJ>
Fixing &&
and ||
will requires partially re-writing the parser and the interpreter. There are no immediate plans for such a fix.
LJ> var obj = {alphas: {a: 'apple', b: 'ball'}};
LJ> obj.alphas.a; // works as expected
"apple"
LJ> obj .alphas .a; // also works as expected (leading dots)
"apple"
LJ> obj. alphas. a; // doesn't yet work (trailing dots)
SyntaxError: unexpected trailing . (dot) obj.
LJ> // This issue usually pops up in refinements spread over multiple lines:
LJ> obj.alphas.
... a;
SyntaxError: unexpected trailing . (dot) obj.alphas.
LJ>
The workaround is to use leading dots only.
Due to JavaScript's semicolon insertion, using leading dots may change the meaning of your program (in JavaScript). Thus, spreading refinements over multiple lines is not advisable.
LittleJ does not (and will not) include semicolon insertion. This is one area where the semantic meaning of a LittleJ program may not be retained in JavaScript. Ways of mitigating this issue are currently being explored.
Thank you for showing interest in Jispy.
You are welcome to experiment with Jispy. PLEASE FEEL FREE TO CONTRIBUTE.
If you use Jispy in any of your projects, PLEASE LET US KNOW.