This repository contains a Javascript transpiler for a simplified version of the LISP language. It was created using Feathers (nodeJS) as the backend framework and other tools such as Lex for the lexical analysis and Jison for the parser.
Give it a try at http://lisptojavascript.com
See the full list of test programs here.
I'll be adding new ones soon.
Lisp:
(defun factorial (n)
(if (= n 0)
(return-from factorial 1)
(return-from factorial (* n (factorial (- n 1))))
)
)
(print (factorial 10))
Transpiles to:
function factorial(n) {
if (n === 0) {
return 1
} else {
return n * factorial(n - 1)
};
};
console.log(factorial(10));
Lisp:
(defvar count 10)
(loop (if (> count 1)
(setq count (- count 1))
(progn
(setq count (- count 1))
(print "last iteration")))
(when (> count 0) (return count)))
Transpiles to:
var count = 10;
while (count > 0) {
if (count > 1) {
count = count - 1;
} else {
count = count - 1;
console.log("last iteration")
};
};
See the grammar in BNF notation by clicking here
Basic arithmetic is supported: addition, subtraction, multiplication and division.
(+ 3 2) //3 + 2
(- a b) //a - b
(- a (* b 3)) // a - b * 3
(/ (* a 2) (- a (+ 3 5))) // a * 2 / a - 3 + 5;
Gobal and local variables are supported using defvar
and let
(defvar a 2) ; var a = 2;
(defvar b 3) ; var b = 3;
(defvar c (- a (* b 3))) ; var c = a - b * 3;
(let c b) ; let c = b;
Values can be assigned to variables during declaration (see above) or using setq
.
In this version of lisp, variable names can only contain letters.
(setq a 2) ; a = 2;
(setq c (- a (* b 3))) ; c = a - b * 3;
(setq x (sum y z)) ; x = sum(y, z) if sum is a declared function
The following operators are supported: >
, <
, =
, and /=
(> a b) ; a > b
(< a 1) ; a > 1
(= a (+ 2 2)) ; a === 2+2
(/= 1 2) ; 1 !== 2
Conditions also support complex logical operations using and
, or
, and not
(or 1 2) ; (1 || 2)
(not b) ; !b
(and (or a b) c) ; ((a || b) && c)
( not (and (or a b) c)) ; !((a || b) && c)
(or (< a b) (> b a)) ; (a < b || b > a)
Syntax:
(if <condition> <sentence_then> <sentence_else>)
Lisp only allows to have one sentence in the then
branch and one sentence in the else
branch. If you need to execute several sentences within any of the branches, you can use the special sentence progn
:
(progn <list_of_sentences>)
Simple if-else sentence:
(if (> a b) (setq b a) (setq a b))
Is equivalent to:
if (a > b) {
b = a;
} else {
a = b;
};
Using prog-n
we can have multiple sentences in any of the branches:
(if (= a b)
(progn
(setq a 0)
(setq b 0))
(progn
(print a)
(print b))
)
if (a === b) {
a = 0;
b = 0;
} else {
console.log(a);
console.log(b)
};
Syntax for loops:
(loop <list_of_sentences> [(when <condition> return <expression>)])
(loop
(setq a (+ a 1))
(when (> a 10) (return a))
)
Is equivalent to:
while (a > 10) {
a = a + 1;
};
If the when
clause is not provided, it will produce an infinite loop:
(loop
(setq a (+ a 1))
)
Javascript:
while (true) {
a = a + 1;
};
( <expression> [<expression>]* )
OR
'( <expression> [<expression>]* )
-
When the list is not preceded by a single quote and the first expression is an
IDENTIFIER
:- If the
IDENTIFIER
matches a declared function, the list is evaluated as a function call. The rest of the expressions will be passed as parameters of the function (see function calls). - Otherwhise, the list is converted to an array
- If the
-
When the list is preceded by a single quote, the list is converted to an array
(defun sum (num) (+ num 1))
(1 2 3)
'(1 2 3)
(sum 1 2)
'(sum 1 2)
(a 1 2)
Transpiles to:
function sum(num) {
num + 1;
};
[1, 2, 3];
[1, 2, 3];
sum(1, 2);
[sum, 1, 2];
[a, 1, 2];
Functions are declared using the token defun
.
Syntax for function declaration:
(defun <function_name> (<params>) <list_of_sentences>)
Where:
<function_name>
: should be an identifier (one or more letters)<params>
: are zero or more expressions separated by blanks<list_of_sentences>
: one or more sentence (variable declaration, assignment, function declaration, function call, arithmetic operations, etc)
Important: a function in LISP returns the value of the last expression evaluated as the return value, but this is not supported in this simplified LISP version. Instead, to return a value from a function it is required to use the return-from
statement explicitly.
return-from myfunction 10
Sum two numbers:
(defun sum (a b)
(return-from sum (+ a b))
)
Transpiles to:
function sum(a, b) {
return a + b;
};
Recursive functions are also supported. The following function:
(defun printUntilZero (a)
(if (> a 0)
(progn
(print a)
(setq a (- a 1))
(printUntilZero a))
(print a))
)
(printUntilZero 10)
Transpiles to:
function printUntilZero(a) {
if (a > 0) {
console.log(a);
a = a - 1;
printUntilZero(a)
} else {
console.log(a)
};
};
printUntilZero(10);
Syntax for function calls:
(<function_name> <params>)
Where:
<function_name>
: must be an identifier (one or more letters) and should match an already declared function. Otherwhise, the sentence will be recognized as a list.<params>
: zero or more expressions
(defun sumThreeNumbers (a b c)
(return-from sumThreeNumbers (+ a (+ b c)))
)
(sumThreeNumbers 10 5 1)
Transpiles to:
function sumThreeNumbers(a, b, c) {
return a + b + c;
};
sumThreeNumbers(10, 5, 1);
This solution only validates that the lisp programs are syntactically correct but I don't do any semantic validations. In other words, I can't guarantee that your code makes sense :-)
For instance, if you declare a function that expects two arguments and you call it with 1 argument:
(defun sumTwoNumbers (a b)
(return-from sumTwoNumbers (+ a b))
)
(sumTwoNumbers 10)
The code is valid syntactically and will generate the following javascript:
function sumTwoNumbers(a, b) {
return a + b;
};
sumTwoNumbers(10);
Although it doesn't make sense.
This is not a bad thing but this kind of behaviors can be avoided using a symbol table during transpiling time. However, it was not worth the effort.
I built the grammar trying to solve one thing at a time: variable declarations, variable assignments, functions, loops, arithmetics, etc.
But I realized that I didn't spend much time trying to understand the LISP semantics first, and that's why my grammar brought me some troubles at the time to enforce the semantic described by LISP.
Some features are hard to support with my current grammar, like the single quote operator that has much more functionality in the original LISP, or even the n-ary operators like +
, -
, *
, /=
, >
, <
, etc. My version currently supports only binary operators.
A better grammar could be provided in the future.
Since list elements are separated by one or more blanks (white spaces), the blank
should be considered as a token the same way we do it with numbers or letters. For that reason, I've been forced to include the blanks in my grammar.
At first, my grammar was a mess. I had to take into account every possible situation in which the blanks could appear. The result was an unflexible grammar - which was very "blank sensitive".
For instance, this was valid:
(defvar a 1)
But this wasn't:
( defvar a 1)
First I added a new step before the transpiling process. The whole idea was to pre-process the input to remove all the conflictive blanks between parenthesis
/*
* Code preprocessing:
* 1 - Remove all repeated blanks
* 2 - Remove conflictive blanks before
* and after parenthesis
* 3 - Remove conflictive blanks before and
* after new lines
*/
lispCode =
lispCode
.replace(/ +/g, ' ')
.replace(/\( +/g, '(')
.replace(/ +\)/g, ')')
.replace(/\) +\(/g, ')(')
.replace(/ +\n/g, '\n')
.replace(/\n +/g, '\n')
.replace(/\) +/g, ')')
.replace(/ +\(/g, '(');
After that, I fixed the grammar by removing all the BLANKS
expected right before or after a parenthesis symbol, because the parenthesis is a disambiguator.
The result:
The code is not blank sensitive anymore, and the grammar is now more readable, maintainable, and scalable.
In LISP functions, the last expression is returned.
At first, I was adding a return sentence to the last expression of a function to return the value. That strategy worked fine except for the cases where the last sentence was a branch: the transpiler was adding a return
right before the IF-THEN
sentence. It was wrong.
That's why I decided that the values should be returned explicitly by the programmer using the return-from
statement.
Because of the limitations of my grammar, I wasn't able to distinguish a list from a function call (both looks pretty similar). The way I decided to approach this is by checking if the first expression of the list was an identifier matching a declared function. If it was, the list is considered as a function call.
- URL: www.lisptojavascript.com/convertToJS
- Description: converts lisp code into javascript code.
- Requires:
lispCode
(string) in JSON body. - Returns: the javascript equivalent code if the lisp is valid, and also the list of tokens and lexemes found.
Response:
{
"success": "<Boolean>",
"lisp": "<String>",
"javascript": "<String>",
"tokens": ["<String>"],
"lexemes": ["<String>"],
"errors": ["<String>"]
}
POST /convertToJS HTTP/1.1
Host: www.lisptojavascript.com
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 504d10e0-9213-4773-bc97-0f9ea2b3eb13
{
"lispCode":"(defvar a 1)"
}
Response:
{
"success": true,
"lisp": "(defvar a 1)",
"javascript": "var a = 1;\n",
"tokens": [
"(",
"DEFVAR",
"BLANK",
"IDENTIFIER",
"BLANK",
"CONST_INT",
")",
"CONST_INT"
],
"lexemes": [
"(",
"defvar",
" ",
"a",
" ",
"1",
")",
""
],
"errors": []
}
POST /convertToJS HTTP/1.1
Host: www.lisptojavascript.com
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 504d10e0-9213-4773-bc97-0f9ea2b3eb13
{
"lispCode":"defvar a 1)"
}
Response:
{
"success": false,
"lisp": "defvar a 1)",
"tokens": [
"DEFVAR"
],
"lexemes": [
"defvar"
],
"errors": [
"Parse error on line 1: Unexpected 'DEFVAR'"
]
}
- URL: www.lisptojavascript.com/isValidLisp
- Description: validates if the lisp code provided is sintactically valid.
- Requires:
lispCode
(string) in JSON body.
Response:
{
"isValidLisp": "<Boolean>"
}
POST /isValidLisp HTTP/1.1
Host: www.lisptojavascript.com
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 1f0d09af-7ef4-4321-8061-dca5aaacd483
{
"lispCode":"(defvar a 1)"
}
Response:
{
"isValidLisp": true
}
Getting up and running is as easy as 1, 2, 3.