Arc Forumnew | comments | leaders | submitlogin
1 point by evanrmurphy 3230 days ago | link | parent

I've got something that covers most of the cases now. I used to take a quoted Arc expression and start translating it immediately. Now I do a recursive ssexpand first:

  (def ssexpandif (x)
    (if ssyntax.x ssexpand.x x))

  (def ssexpandall (tree)
    (treewise cons ssexpandif tree))

  (def js args
    (each a ssexpandall.args
      js1.a
      (pr #\;)))
By the time the compiler's primary function (js1) sees x.y, it has already been expanded to (x y). Then the corresponding JS function call can be printed:

  arc> (js `foo.bar)
  foo(bar);
  arc> (js `(foo bar))   ; prints the same
  foo(bar);
For ! ssyntax, document!body now expands to (document 'body) as it should. Then the correct JS is printed:

  arc> (js `document!body)
  document.body;
Which brings us to the star example of this poll:

  arc> (js `(= document!body!innerHTML ((document!getElementById "foo") 'value)))
  (function(){
    return document.body.innerHTML = document.getElementById('foo').value;
  })();
It turned out like the one fallintothis proposed. (Thank you, by the way.) Note that that the function wrapping is delibrate. [1]

But how does the compiler differentiate a function call from object access? Well, the implementation is naive: if there is only 1 arg and it's quoted, consider it object access; else it's a function call. So it no longer allows function calls to with a single quoted argument (hence, "something that covers most of the cases"). The only way I could think of to get around this is to check the type of the item in functional position, but that can't be done until runtime. Any ideas?

: ssyntax works now too! I defined compose and you can see that the equivalent expressions print the same:

  arc> (js `(car:cdr (list 1 2 3)))
  (function(){
    var g13954=arraylist(arguments);
    return apply(cdr,g13954).car;
  })(list(1,2,3));
  arc> (js `((compose car cdr) (list 1 2 3)))
  (function(){
    var g13956=arraylist(arguments);
    return apply(cdr,g13956).car;
  })(list(1,2,3));
You wouldn't be able to tell that they work from this, of course, since you're reading a macroexpansions with gensyms. (Plus some of the functions are defined in JavaScript.) When executed in a browser, though, they both evaluate to 2.

So I guess the problem that inspired this poll is mostly solved now! That is, unless I've neglected something critical, made a terrible mistake and need to revert. ^_^

[1] (= x y) should compile to (function(){return x=y;})() rather than x=y for the same reason (if x y z) should compile to (x?y:z) rather than if(x)y;else z;. [2] (Thanks, rocketnia.)

[2] http://arclanguage.org/item?id=11920



2 points by rocketnia 3230 days ago | link

Sounds pretty awesome. ^_^ I do have some concerns, though.

For one thing, I'm kinda concerned that you'll get

  arc> (ssexpandall ''dont-expand-me.im-quoted)
  (quote (dont-expand-me im-quoted))
but there's probably little need to use quotation in JavaScript anyway.

(= x y) should compile to (function(){return x=y;})()

I don't know why you suppose (x=y) wouldn't work for that case, but at least your approach will generalize consistently to the case (= x y z w). ^_^

On another note, you may want to change your function block format to "(function(){ ... }).call(this)". That way this has the same value inside and outside the block. This is especially relevant for something as basic as assignment; if I'm planning to use a function as a constructor and I say things like (= this!x 10), I'll be frustrated if I end up setting properties on the window object. :-p

-----

1 point by evanrmurphy 3230 days ago | link

> at least your approach will generalize consistently to the case (= x y z w).

Yes, that's the reason. (= x y) was a poor example since you don't actually need the wrapping function for single assignment. In fact, I've provided a way to do it without:

  arc> (js `(assign x y))
  x=y;
Feels like cheating to compile assign to = (if you did it in Arc, you'd have a circular definition! ^_^), but I don't think JavaScript provides a more primitive assignment operator.

> That way this has the same value inside and outside the block.

Very astute! ^_^ I had become aware of the problem of this changing values but didn't know how to fix it. I will try your 'call approach soon. Thanks a lot!

  arc> (ssexpandall ''dont-expand-me.im-quoted)
Another good point. Something else was making me question my ssexpandall formulation so this is going on my TODO. I think you're right that quotation isn't critical in JavaScript, but I do want to compile it correctly. I hope to eventually support eval:

  arc> (js `(eval '(alert (eval '(+ 1 2)))))
  eval('alert(eval(\'(1+2)\'))');
I doubt quasiquotation will be supported though. (How would you compile that anyway, string concatenation?) I don't think JavaScript has anything quite like quasiquotation, which is probably why it doesn't have macros (which is in large part why lisp=>js compilers are attractive in the first place [1]). Additionally, there's an elegance to reserving unquote for escaping Arc code, which would be difficult to do if you compiled quasiquotation.

Edit: Hmm... that last paragraph may not be very well reasoned. Maybe string concatenation - or rather concatenating strings with unquoted expressions - is in fact a good counterpart to quasiquotation and I should compile it, even though JavaScript doesn't have macros. What do you think?

[1] http://michaux.ca/articles/macros-in-javascript-please

-----

2 points by rocketnia 3229 days ago | link

I hope to eventually support eval

I think the way you're going to go about it, by having (quote ...) forms be compiled, has a bit of a caveat. If you're already planning to have a!b expand to (a 'b) and compile to "a.b", then won't (js '(eval 'foo)) just result in "eval.foo"?

Maybe a!b should expand to (ref a "b") or something, where (ref a b) compiles to "a[b]" for most arguments but compiles to "a.contentsOfB" when the second argument is a literal string that counts as a JavaScript identifier. (The second case could be totally left off to make things easier; document['getElementById']('foo') is still a method call, and regular property gets and sets work too.)

All that being said, I bet you already support eval(), in a way:

  what would be    (js '(eval '(alert (eval '(+ 1 2)))))
  is expressed as  (js `(eval ,(js `(alert (eval ,(js '(+ 1 2)))))))
The difference here is just syntax sugar, IMO. (Saving parentheses is a fine goal of syntax sugar, though!)

Maybe string concatenation ... is in fact a good counterpart to quasiquotation and I should compile it...

That feature would be a bit more difficult to simulate if 'js didn't support it intrinsically. Here's a quick approach:

  (mac jswith (bindings . body)
    `(fn-jswith (list ,@(map .1 bindings))
                (fn (,(map .0 bindings)) ,@body)))
  
  (def fn-jswith (vals body)
    ; We're adding the suffix "v" to each name so that it isn't the
    ; prefix of any other name, as might happen with gs1234 and gs12345,
    ; for instance. Note that it still counts as a JavaScript identifier
    ; with this suffix.
    (withs (strnames  (map [string (uniq) 'v] vals)
            names     (map sym strnames))
      `( (fn ,names
           (eval ,(multisubst (map [list (+ "('+" _ "+')") _)] strnames)
                    (js do.body.names))))
         ,@vals)))
  
  (mac jslet (var val . body)
    `(jswith (,var ,val) ,@body))
  
  now what would be  (js '(eval `(+ 1 ,foo)))
  
  is expressed as    (js `(eval ,(jslet f 'foo
                                   (js `(+ 1 ,f)))))
  where the final form sent to 'js is
    (eval ((fn (gs1001v) (eval "'(1+('+gs1001v+'))'")) foo))
  
  or expressed as    (js:jslet f 'foo
                       (js `(eval ,(js `(+ 1 ,f)))))
  where the final form sent to 'js is
    ((fn (gs1001v) (eval "'eval(\'(1+('+gs1001v+'))\')'")) foo)
I do feel that this difference is more than sugar, since the 'foo subexpression is moved out of context.

Also, more importantly, it has a security leak I'm not sure how to fix. The call to 'subst doesn't pay attention to the meaning of what it's replacing. If an attacker is able to get a string like "gs1001v" into a forum post or username or whatever in the server data, and then that string is embedded as a literal string in JavaScript code which is processed as the body of a 'jslet, something wacky might happen, and the attacker will be in a position to arrange things so that just the wrong wacky things happen.

If you just make a way to put identifiable "holes" in the compiled JavaScript, you'll remove the need to resort to blind string substitution here. The holes could be as simple as names surrounded by delimiters which you guarantee not to appear elsewhere in the result (even in string literals); that way a string substitution approach doesn't have to be blind. The holes could help you implement 'quasiquote, and conversely, if you implement 'quasiquote, there might not be much of a need for the holes.

Additionally, there's an elegance to reserving unquote for escaping Arc code, which would be difficult to do if you compiled quasiquotation.

Well, the Arc quasiquotes are processed before 'js even sees the input, right? Here's the only problem I see (and maybe it's exactly what you're talking about):

A typical way to escape from two levels of nested Arc quasiquotes is ",',", as in `(a `(b ,c ,',d)). That constructs something that includes a (unquote (quote ...)) form, so it only works when you're sending the result somewhere where unquote-quotes don't matter (like, to be evaluated as Arc). So ideally, the 'js meanings of 'quasiquote and 'quote should have this property. I don't think this would be especially hard to guarantee, but it might be easy to miss.

(Note that if Arc's quasiquotes didn't nest, the same example would be expressed as `(a `(b ,',c ,d)), and no unquote-quote would hang around to be a problem. I'm beginning to wonder if nesting quasiquotes are a wart of Arc.)

-----

4 points by fallintothis 3230 days ago | link

If you want it, I wrote ssexpand-all for http://arclanguage.org/item?id=11179 and handled quotes, plus another edge case that leads to infinite loops:

  arc> (ssyntax 'a~b)
  t
  arc> (ssexpand 'a~b)
  a~b
http://bitbucket.org/fallintothis/contract/src/tip/test/ssyn...

-----

1 point by evanrmurphy 3226 days ago | link

It appears I've reinvented a worse version of your wheel. ^_^

Your ssexpand-all is superior and I'm using it now. I did try to refactor it, thinking there must be a function f (like my ssexpandif but more sophisticated) that satisfies

  (treewise cons f expr)
while producing the same functionality, but I haven't been able to determine what that would be.

-----

4 points by fallintothis 3226 days ago | link

thinking there must be a function f (like my ssexpandif but more sophisticated)

Not quite. The problem here is not f (entirely), but also treewise.

  (def treewise (f base tree)
    (if (atom tree)
        (base tree)
        (f (treewise f base (car tree)) 
           (treewise f base (cdr tree)))))
treewise will only act on atoms, whereas we need to act on conses sometimes -- namely, quoted items. If we ignore those, it could work:

  (def ssexpand-all (expr)
    (treewise cons
              [let expanded (ssexpand-if expr)
                (if (is _ expanded)
                    _
                    (ssexpand-all expanded))]
              expr))
But ssyntax can expand into more ssyntax; e.g.,

  arc> (ssexpand 'a:.b)
  (compose a .b)
  arc> (ssexpand '.b)
  (get b)
So, we need to recurse in the f argument anyway. At a certain point, it seems like the anonymous & higher-order functions add layers of indirection on what should just be a straightforward recursive definition.

I get really annoyed at that, though, when working with trees in Arc. There always seems to be some underlying pattern that's just different enough that I can't abstract it into a higher-order function.

-----

3 points by evanrmurphy 3226 days ago | link

> you may want to change your function block format to "(function(){ ... }).call(this)".

This is done now. Using your example:

  arc> (js `(= this!x 10))
  (function(){return this.x=10;}).call(this);nil
All I did was change do's definition in js was accordingly:

  (js-mac do args
    `(((fn () ,@args) 'call) this))

  ; used to be

  ;(js-mac do args
  ;  `((fn () ,@args)))
  
js-mac just adds its args to table of pseudo-macros for js:

  (= js-macs* (table))

  (mac js-mac (name args . body)
    `(= (js-macs* ',name) (fn ,args (js1 ,@body))))
Most of arc.arc's macros have been copied verbatim to js.arc as js-mac definitions, after which they're ready to use. For example,

  (js-mac with (parms . body)
    `((fn ,(map1 car (pair parms))
        ,@body)
      ,@(map1 cadr (pair parms))))
lets us use with in our JavaScript:

  arc> (js `(with (x 1 y 2)
              (+ x y)))
  (function(x,y){return (x+y);})(1,2);
EDIT: Just changed with's definition to

  (js-mac with (parms . body)
    `(((fn ,(map1 car (pair parms))
         ,@body)
       'call) this ,@(map1 cadr (pair parms))))
for the same reason we changed do. I should find a better way to refactor so we're not handwriting the .call(this) form everywhere. Anyway, now it's

  arc> (js `(with (x 1 y 2)
              (+ x y)))
  (function(x,y){return (x+y);}).call(this,1,2);

-----