Arc Forumnew | comments | leaders | submitlogin
Arc2js, an Arc to JavaScript compiler (github.com)
4 points by Pauan 4864 days ago | 12 comments


1 point by Pauan 4863 days ago | link

And here is a fantastic example of why macros are incredibly awesome, and why JS could really use macros. Let's suppose you wanted to generate the following DOM structure:

  <div test="yes">
    <div foo="bar">
      <div qux="corge"></div>
      <div corge="yesno"></div>
    </div>
  </div>
Now, you could of course write that out as a string and use innerHTML, etc. But what about event listeners? What if you want one of the node's attributes to vary depending on another node? Now you get into the verbosity that is the DOM. Let's start with the traditional way:

  var div1 = document.createElement("div");
  div1.setAttribute("test", "yes");

  var div2 = document.createElement("div");
  div2.setAttribute("foo", "bar");

  var div3 = document.createElement("div");
  div3.setAttribute("qux", "corge");

  var div4 = document.createElement("div");
  div4.setAttribute("corge", div1.getAttribute("test") + "no");

  div2.appendChild(div3);
  div2.appendChild(div4);
  div1.appendChild(div2);
Okay, so we need to give each element a unique name... and we need to deal with accurate-but-verbose names like setAttribute... and this is all flat: it doesn't show the nested structure of the DOM. So when working on my Chrome extensions, I use a little library that provides a UI.create function:

  UI.create("div", function (x) {
      x.setAttribute("test", "yes");

      x.appendChild(UI.create("div", function (self) {
          self.setAttribute("foo", "bar");

          self.appendChild(UI.create("div", function (self) {
              self.setAttribute("qux", "corge");
          }));

          self.appendChild(UI.create("div", function (self) {
              self.setAttribute("corge", x.getAttribute("test") + "no");
          }));
      }));
  });
This shows the structure of the DOM, and due to lexical scope, I can give every element the same name if I want to. I consider this an improvement over the standard DOM, but it's still far too verbose...

But with my compiler, I wrote a "div" and "w/div" macro, so now I can write this:

  (w/div x test "yes"
           (div foo "bar"
             (div qux "corge")
             (div corge (+ (x.getAttribute "test") "no"))))
Well, gosh. Not only is this drastically shorter and easier to read... but it shows the DOM's nested structure. w/div lets you create a new div with a specific name (in this case, "x"), and div is just like w/div, except it prechooses the name "self" for you. You can think of them like rfn and afn, but for DOM elements.

And since this macro expands into the verbose DOM methods, I can do things like add event listeners. Thus, this method is by far the best: you get all the flexibility of the DOM, but with minimal verbosity.

For instance, I just added in this:

  (div on (click (e)
            (+ this.id "bar")))
Which then expands into this:

  (function (self) {
      self.addEventListener("click", (function (e) {
          return (this.id + "bar");
      }), true);
      return self;
  })(document.createElement("div"));

-----

1 point by Pauan 4863 days ago | link

And now styles work, so this:

  (append document.body
    (div style (height          50px
                width           50px
                backgroundColor black)

         on    (click (e)
                 (alert "foo")))

    (div style (height          50px
                width           50px
                backgroundColor green)

         on    (click (e)
                 (alert "bar"))))
Is compiled into this:

  document.body.appendChild((function (self) {
      self.style.height = "50px";
      self.style.width = "50px";
      self.style.backgroundColor = "black";
      self.addEventListener("click", (function (e) {
          return alert("foo");
      }), true);
      return self;
  })(document.createElement("div")));
  
  document.body.appendChild((function (self) {
      self.style.height = "50px";
      self.style.width = "50px";
      self.style.backgroundColor = "green";
      self.addEventListener("click", (function (e) {
          return alert("bar");
      }), true);
      return self;
  })(document.createElement("div")));

-----

1 point by Pauan 4864 days ago | link

I have started work on an Arc to JavaScript compiler. Unlike ArcLite, this does not use an interpreter written in JS. Instead, the Arc code is translated directly into a JavaScript string, which can then be evaluated in your favorite JS interpreter (V8 is quite fast).

I actually just started this today, so naturally it may be buggy and is missing a lot of functionality, but basic functions, function calls, and macros work:

  (with (a 1 b 2 c 3)
    (list a b c))
Becomes:

  (function (a, b, c) {
      return [a, b, c];
  })(1, 2, 3)
Because this is a direct one-to-one compiler, I think it would be more accurate to think of it as a thin Arc-like syntax for JS, rather than as a port of Arc. Basically, this is just like CoffeeScript, but for Arc.

So, if you're used to Arc but have never programmed in JavaScript, certain things will trip you up. For starters, all numbers in JS are 64-bit floating point, there are no ints. Thus, Arc ints become JS floating points.

Also, strings in JS may differ somewhat from Arc strings, but they should be reasonably similar (both support \n and \t syntax, etc.) One difference is that JS supports both ' and " for strings, but Arc only supports "

Also, JS has a lot of things that evaluate to false: "", null, undefined, false, NaN, and 0

Arc only has nil, everything else is true.

---

I still need to get table notation working. Once that's done, you should be able to do stuff like this:

  (= foo (document!createElement "div"))
  (foo!setAttribute "bar" "qux")
Which should compile into the following:

  var foo = document["createElement"]("div");
  foo["setAttribute"]("bar", "qux");
---

Because it's just a thin skin over JS and not a full port, the primary reason you would want to use arc2js is if you want to program in JS, but you like Arc syntax and macros.

Which reminds me. Because the compiler does not have access to the JS environment, there is a clean separation between JS and Arc code: Arc macros can call any Arc function, but JS code can only call JS code.

Unfortunately, that means the following won't work:

  (= browser ...) ; code to detect what the browser is

  (mac something ()
    (case browser
      ie     ...
      gecko  ...
      webkit ...
      opera  ...))
Because the macro "something" doesn't have access to the JS environment, it can't expand into different code depending on what the JS code does. It'll have to do the browser check at runtime.

-----

1 point by Pauan 4864 days ago | link

And now things like `assign`, `if`, and `is` work. Which means that the `in` macro also works:

  (in x "foo" "bar" "qux")
Becomes:

  (function (g1585) {
      return (function (g1586) {
          return (g1586 ? g1586 : (function (g1587) {
              return (g1587 ? g1587 : (function (g1588) {
                  return (g1588 ? g1588 : undefined);
              })((g1585 === "qux")));
          })((g1585 === "bar")));
      })((g1585 === "foo"));
  })(x)
Atrociously inefficient and verbose, obviously, but it does work. And when I add in `or` it should become clearer.

---

EDIT: I just added in `or`. Now the output is a lot more readable:

  (function (g1590) {
      return ((g1590 === "foo") || (g1590 === "bar") || (g1590 === "qux"));
  })(x)
Also, you may have noticed that the output includes the actual gensym name. This is unfortunate, but JS doesn't have gensyms, so it's the best we can do.

-----

1 point by Pauan 4864 days ago | link

I just added in some more stuff. First off, you can now write your own macros. So for instance, let's say you had this file:

  (mac w/element (n x . body)
    `(let ,n (document.createElement ,x)
       (do ,@body)
       ,n))

  (mac w/div (x . body)
    `(w/element ,x "div" ,@body))

  (w/div x
    (x.appendChild
      (w/div x
        (x.setAttribute "foo" "bar"))))
It would then compile into this JavaScript:

  (function (x) {
      x.appendChild((function (x) {
          x.setAttribute("foo", "bar");
          return x;
      })(document.createElement("div")));
      return x;
  })(document.createElement("div"))
Notice how "w/div" and "w/element" don't appear at all in the JS: they're expanded away by the compiler. In case you're curious, the above code generates the following DOM structure:

  <div>
    <div foo="bar"></div>
  </div>
---

Another nifty change I did was add in a couple optimizations for common cases. For instance, before, if you used this:

  (def foo ()
    (let a 50
      (+ a 10)))
It would compile into this:

  var foo = (function () {
      return (function (a) {
          return (a + 10);
      })(50);
  });
This is correct in all circumstances, but it's inefficient in the very common circumstance of a let inside a function. So I optimized it so it now compiles into this:

  var foo = (function () {
      var a = 50;
      return (a + 10);
  });
Aside from the superfluous parentheses, that code is just as fast as if you had manually wrote it! I also optimized `do` within functions:

  (def foo ()
    (do a
        b
        c))
Used to be:

  var foo = (function () {
      return (function ()
          a;
          b;
          return c;
      })();
  });
But now it's:

  var foo = (function () {
      a;
      b;
      return c;
  });
---

Lastly, I added in an "arc2js" function, that lets you convert an entire file from Arc to JS:

  (arc2js "/path/to/foo.arc" "/path/to/foo.js")

-----

1 point by Pauan 4864 days ago | link

Just added in optional and rest args for functions:

  (fn (a b (o c 5))
    (list a b c))

  (function (a, b, c) {
      c = c || 5;
      return [a, b, c];
  })


  (fn args args)

  (function () {
      var args = Array.prototype.slice.call(arguments);
      return args;
  })


  (fn (a b . c)
    (list a b c))

  (function (a, b) {
      var c = Array.prototype.slice.call(arguments, 2);
      return [a, b, c];
  })
I think that's enough for one day. I'm not sure if a simple boolean test is enough for optional args. Perhaps they should test explicitly against undefined?

-----

3 points by akkartik 4864 days ago | link

evanrmurphy did some work in this area: http://arclanguage.org/item?id=13775

-----

2 points by evanrmurphy 4863 days ago | link

Don't forget rocketnia's awesome in-browser REPL:

http://rocketnia.kodingen.com/af/try-lava-script/

It shows you the compiled JavaScript for your input expression as well as the evaluated result. For example:

  ; Defining a function
  > (= add1 (fn(x­) (+ 1 x)))        
  add1=(function(x){return 1+x;})   
  => function (x){return 1+x;}      

  ; Calling the function
  > (add1 2)
  add1(2)
  => 3

-----

1 point by Pauan 4863 days ago | link

Ah, nice.

-----

1 point by Pauan 4864 days ago | link

Oh yeah, and another neat thing: my compiler knows the difference between necessary and superfluous whitespace. By default it pretty-prints it, so the JavaScript looks nice. But you can use a mode that will minify it for you:

  (w/whitespace minify (tojs '(fn () (let a 50 a))))

  ->

  (function(){var a=50;return a;})
You can also manually tweak the indent level, line separator, etc. Not bad for a single day's work, eh?

-----

1 point by Pauan 4864 days ago | link

Also, I would like to note that some Arc macros work out of the box with my compiler. As already shown, `let` and `with` work, since they expand into functions. Another one that works fine is rfn/afn. And even ++ works:

  (def foo ()
    (let a 10
      (++ a 5)
      (+ a 30)))


  var foo = (function () {
      var a = 10;
      a = (a + 5);
      return (a + 30);
  });

-----

1 point by aw 4864 days ago | link

This looks useful.

-----