| I've been working on a JavaScript compiler for Arc. It is far from finished, but I've already been sitting on it for awhile and know some peer review would be a good idea, or as akkartik put it: "Seems stupid to be working alone like an alchemist when I have the perfect community to get feedback from." [http://arclanguage.org/item?id=11869] To start with something familiar, here's js.arc (with html.arc) doing the Arc Challenge: (defop said ()
(tag script
(js `(def submit ()
(= foo (document.getElementById "foo")
document.body.innerHTML
,(tostring
(jslink "click here"
`(= document.body.innerHTML
(+ |\'you said: \'| foo.value)))))
nil)))
(inputid "foo")
(jsbut `(submit)))
This is kludgier than it could be, (defop said ()
(inputid "foo")
(jsbut `(= document.body.innerHTML
,(tostring
(jslink "click here"
`(= document.body.innerHTML
(+ "you said: "
(document.getElementById "foo").value)))))))
but issues with nested strings and dot ssyntax keep this from being possible at the moment.Of course neither of the above competes with srv.arc for terseness, but then I guess it's misleading to cast two as competitors anyway since they're meant to work together. 'jsbut and 'jslink are a couple additions to html.arc just to make the onclick attribute more convenient: (def jslink (text (o clk text) (o dest "#"))
(tag (a onclick (tostring:js clk) href dest) (pr text)))
(def jsbut (clk (o text "submit") (o name nil))
(gentag input onclick (tostring:js clk)
type 'submit name name value text))
To better illustrate how js.arc works, how about seeing some simple expressions compile at the REPL? js.arc currently uses stdout like html.arc rather than return values, so I'll omit those for readbility: arc> (js '1)
1
arc> (js "foo")
'foo'
arc> (js '(+ 1 1))
(1+1)
arc> (js '(+ 1 (/ 2 3) (* 4 5) (mod 6 7)))
(1+(2/3)+(4*5)+(6%7))
arc> (js '(fn (x) x))
function(x){return x;}
arc> (js '(def foo (x) x))
function foo(x){return x;}
arc> (js '(foo 1))
foo(1)
arc> (js '(do (foo 1)
((fn (x) x))))
(function(){foo(1);return (function(x){return x;})();})()
arc> (js '(if a b c d e))
if(a)b;else if(c)d;else e;
arc> (js '(and x (or y z)))
(x&&(y||z))
arc> (js '(let x 1
(alert x)))
(function(x){return alert(x);})(1)
arc> (js '(with (x 1 y 2)
(document.write (+ x y))))
(function(x,y){return document.write((x+y));})(1,2)
arc> (js `(= x.innerHTML ,(tostring (tag html
(tag body
(tag p (pr "hello world")))))))
(x.innerHTML='<html><body><p>hello world</p></body></html>')nil
And here's the complete source for js.arc: ; TODO
; fix nested strings/escaping, esp. to work with html.arc
; implement 'expand= etc. for more robust '=?
; for, while, switch/case, afn/rfn/accum, cons, quote/unquote?
; figure out js objects
; make js objects into arc tables so we do (document!getElementById x) for document.getElementById(x)
; or use fns so (document.getElementById x) is really ((document getElementById) x)
; maybe allow dot after fn call like (document.getElementById x).value
; triple-check semicolons
(mac w/braces body
`(do (pr #\{) ,@body (pr #\})))
(mac w/parens body
`(do (pr #\() ,@body (pr #\))))
(mac w/quotes body
`(do (pr #\') ,@body (pr #\')))
(mac w/semi body
`(do ,@body (pr #\;)))
(def js-infix (op args)
(w/parens (between arg args (js op)
(js arg))))
(def js-fn (args body)
(pr "function")
(w/parens
(between arg args (pr #\,)
(js arg)))
(w/braces
(each s body
(w/semi
(if (is s (last body))
(do (pr "return ")
(js s))
(js s))))))
(def js-def (name args body)
(pr "function ")
(js name)
(w/parens
(between arg args (pr #\,)
(js arg)))
(w/braces
(each s body
(w/semi
(if (is s (last body))
(pr "return "))
(js s)))))
; old 'js-do used block, fn or block better? seems block more readable but fn matches arc's 'do
(def js-do (args)
(js `((fn () ,@args))))
; need to handle case of (len args) < 2?
(def js-if (args)
(do (pr "if")
(w/parens (js (car args)))
(w/semi (js (cadr args)))
((afn (xs)
(if (no xs)
nil
(cadr xs)
(do (pr "else if")
(w/parens (js (car xs)))
(w/semi (js (cadr xs)))
(self (cddr xs)))
(do (pr "else ")
(w/semi (js (car xs)))
(self (cddr xs)))))
(cddr args))))
(def js-when (test body)
(js `(if ,test (do ,@body))))
(def js-unless (test body)
(js `(if (no ,test) (do ,@body))))
(def js-with (parms body)
(js `((fn ,(map1 car (pair parms))
,@body)
,@(map1 cadr (pair parms)))))
(def js-let (var val body)
(js `(with (,var ,val) ,@body)))
(def js-withs (parms body)
(if (no parms)
(js `(do ,@body))
(js `(let ,(car parms) ,(cadr parms)
(withs ,(cddr parms) ,@body)))))
(def js-= (args)
((afn (args)
(if (no args)
nil
(cadr args)
(do
(w/parens
(js (car args))
(pr "=")
(js (cadr args)))
(if (cddr args)
(pr #\;))
(self (cddr args)))))
args))
(def js args
(each s args
(w/uniq (ga gs)
(if (isa s 'string) (w/quotes (pr s))
(atom s) (pr s)
(atom (car s))
(if (in (car s) '+
'- '* '/ '>=
'<= '> '<) (js-infix (car s) (cdr s))
(case (car s)
mod (js-infix '% (cdr s))
is (js-infix '=== (cdr s))
and (js-infix '&& (cdr s))
or (js-infix '\|\| (cdr s))
fn (js-fn (cadr s) (cddr s))
def (js-def (cadr s) (car:cddr s) (cdr:cddr s))
do (js-do (cdr s))
if (js-if (cdr s))
when (js-when (cadr s) (cddr s))
unless (js-unless (cadr s) (cddr s))
with (js-with (cadr s) (cddr s))
let (js-let (cadr s) (car:cddr s) (cdr:cddr s))
withs (js-withs (cadr s) (cddr s))
= (js-= (cdr s))
(do (js (car s))
(w/parens (between arg (cdr s) (pr #\,)
(js arg))))))
(is (caar s) 'fn) (do (w/parens (js-fn (cadr:car s) (cddr:car s)))
(w/parens (between arg (cdr s) (pr #\,)
(js arg))))))))
It was satisfying after I got some axioms laid down to be able to start copying macro definitions almost verbatim from arc.arc. For example, compare the definition of 'with from arc.arc with the above definition of 'js-with, which just passes the body of the former as an argument to 'js.If you look closely at the above definitions, some are insufficient. The definition of 'js-unless depends on 'no, but 'no isn't defined anywhere else in the file. That's because until now I've neglected to mention a small JavaScript library, arc.js, that I've been using in conjunction with js.arc. It defines 't, 'nil, 'cons, 'car, 'cdr and some other arc.arc functions, and I'll post its souce as well: // js arrays have so many of these functions, wonder if better to use them instead of cons object
// same with null for nil, true for t, && for and, || for or
// make null's toString "nil"
var t = true;
// [], false or undefined instead?
var nil = null;
// should be for any number of args
// redundant because in js.arc now
function is (x,y) {
if (x == y) { return t; }
else { return nil; }}
function no (x) { return is (x, nil); }
function isnt (x, y) { return no (is (x, y)); }
function cons (car, cdr) {
return { car:car,
cdr:cdr,
toString: function () {
return "(" + this.car + " . " + this.cdr + ")"; },
type: 'cons'
}; }
function car (xs) { return xs.car; }
function cdr (xs) { return xs.cdr; }
function caar (xs) { return car(car(xs)); }
function cadr (xs) { return car(cdr(xs)); }
function cddr (xs) { return cdr(cdr(xs)); }
function type (x) { return x.type; }
function acons (x) { return is (type (x), 'cons'); }
function atom (x) { return no (acons (x)); }
function copylist (xs) {
if (no(xs)) {
return nil; }
else { return cons(car(xs), copylist(cdr(xs))); }}
function list () {
var acc = nil;
for (i = arguments.length; i > 0; i -= 1) {
acc = cons (arguments[i-1], acc); }
return acc; }
function idfn (x) { return x; }
function map1 (f, xs) {
if (no (xs)) {
return nil; }
else { return cons (f (car (xs)), map1 (f,cdr (xs))); }}
function pair (xs, f) {
if (!f) { f = list; } // optional arg
if (no (xs)) {
return nil; }
else if (no (cdr (xs))) {
return list (list (car (xs))); }
else { return cons (f (car (xs), cadr (xs)),
pair (cddr (xs), f)); }}
// breaks on invalid keys?
function assoc (key, al) {
if (atom (al)) {
return nil; }
else if (acons (car (al)) && is (caar (al), key)) {
return car (al); }
else { return assoc (key, cdr (al)); }}
function alref (al, key) {
return cadr (assoc (key, al)); }
// listtab for js arrays instead of hashes
// shows how to do afn, rfn
function listarray (xs) {
return (function self (xs, acc) {
if (no (xs)) {
return acc; }
else { return acc.concat (car (xs), self (cdr (xs), acc)); }
}) (xs, []); }
//function join () {
// var args = list.apply (this, arguments);
// if (no (args)) {
// return nil; }
// else {
// (function (a) {
// if (no (a)) {
// join.apply (this, listarray (cdr (args))); }
// else { return cons (car (a), join.apply (this, listarray (cdr (a)), listarray (cdr (args)))); }
// }) (car (args)); }}
// workaround since above not working
function join () {
var acc = [];
for (i = 0; i < arguments.length; i += 1) {
acc = acc.concat (listarray (arguments [i])); }
return acc; }
function rev (xs) {
return (function self (xs, acc) {
if (no (xs)) {
return acc; }
else { return self (cdr (xs), cons (car (xs), acc)); }
}) (xs, nil); }
function alist (x) {
return no (x) || is (type (x), 'cons'); }
// not tested
function reclist (f, xs) {
return xs && (f (xs) || reclist (f, cdr (xs))); }
// seems to return false too often
function recstring (test, s, start) {
if (!start) { start = 0; } // optional arg
return (function self (i) {
return (i < s.length) && (test (i) || self (i+1));
}) (start); }
I'll probably end up merging the two files or finding some better way to organize them, because right now the separation feels a bit arbitrary and awkward. |