(do1 nil (= foo (string:n-of 100000 "@_")))
(defop foo req
(let x copy.foo
(prn ellipsize.x)))
; I'm defining 'bar just to be sure it's the strings themselves which
; consume the memory.
(do1 nil (= bar (string:n-of 10 "@_")))
(defop bar req
(let x copy.bar
(prn ellipsize.x)))
; And here's an op that should really cause a problem.
(do1 nil (= baz (string:n-of 100000 "@_")))
wipe.bazzes
(defop baz req
(let x copy.baz
(push x bazzes)
(prn ellipsize.x)))
(thread:asv)
If I repeatedly request foo, my Racket process's memory usage (Racket 5.0.1 on Windows XP) climbs as high as 50-60 MB, but after another request, it garbage-collects and releases about half the memory. It does reach higher peaks if I keep going, to the point where now it's going up to 120 MB and dropping to 60, but clearly all those strings I'm making are at least eligible for garbage collection, even if the garbage collector lets more and more of them stick around.
So I've seen no evidence of a memory leak yet... but then again, I haven't had an Arc server run for longer than it takes to do poking around like this. Maybe you should try out a more recent version of Racket?
On a side note, I notice you prefix certain things with "(do1 nil", presumably so that it returns nil rather than returning a gigantic string that the REPL will try to print in its entirety. I stick "no:" in front of things for the same purpose. I.e.:
I find this rather handy and thought I'd share it. (On another note, it seems "~" does the same thing as "no:", which I think is not much of an improvement (1 character instead of 3) and probably not worth allocating a whole ssyntax character for.)
Maybe you should try out a more recent version of Racket?
That's a good idea. I'm still using MzScheme 4.2.1. (Although before I go to the trouble, maybe I should skim recent release notes to see if they were supposed to have fixed any memory leak.)
Thanks for doing that test. I just tried again on my case and it got to > 450 MB (90%+ on a 512 machine) without showing signs of GCing! I'll push it even further later to see if it collects; if it does, then maybe I just need to lower a global like threadlimit* if I don't want the entire machine to get consumed.
I'm pretty sure the second (continue) form is expanded after the inner (afor ...) form is expanded, so that it tries to continue the wrong loop, but ends up referring to an unbound variable instead.
I recommend sticking to an anaphoric variable that has no macro wrapping it, sort of like this:
I'm pretty sure the second (continue) form is expanded after the inner (afor ...) form is expanded, so that it tries to continue the wrong loop, but ends up referring to an unbound variable instead.
Very good point, I hadn't tried to nest them before.
I will try your aloop definition when I get the chance.
It sounds like you might be pleased to hear of an Arc utility variously known as 'afnwith, 'loop, or 'xloop: http://awwx.ws/xloop0
The workings behind 'xloop are pretty simple. Just like (with (a 1 b 2) c) expands into ((fn (a b) c) 1 2), (xloop (a 1 b 2) c) expands into ((rfn next (a b) c) 1 2).
Thanks, but next in an xloop doesn't really work as a substitute, right? The code after a (next) still gets executed after the recursion completes, whereas with a continue, the rest of the branch isn't executed. I really need the latter.
(I wrote this response before I read some of the more recent comments, so it'll end up duplicating information, but I'm posting it anyway 'cause of the example utilities.)
In that case, I typically use 'catch or 'point (which indeed use continuations). If you want a loop with the ability to either break or continue, you can put a 'point on the outside and another 'point on the inside.
Here's a half-effort to abstract this away. I don't know how useful it'll be, but it might be the start of something better, so everyone, feel free to use it for anything without credit. ^_^
(mac w/breaks (break next loop-header . body)
(w/uniq g-breakpoint
`(point ,g-breakpoint
(let ,break (fn ((o val)) (,g-breakpoint val))
(,@loop-header
(point ,next
,@body))))))
(mac forever body
`(while t ,@body))
(mac bforever body
`(w/breaks break next (forever) ,@body))
(mac bfor (var start end . body)
`(w/breaks break next (for ,var ,start ,end) ,@body))
; What a name.
(mac beach (var coll . body)
`(w/breaks break next (each ,var ,coll) ,@body))
; Turn any loop into a breakable one. Example usage:
;
; (b-:until (all has-come-home cows) :
; (tip rand-elt.cows)
; (unless whales
; (next))
; (actually-beach rand-elt.whales)
; (when cops
; (prn "Uh, oops?")
; (break)))
;
(mac b- (loop)
(let (header (_ . body)) (split loop (pos ': loop))
`(w/breaks break next ,header ,@body)))
(Clearly, this wouldn't mix very well with 'xloop; since an 'xloop doesn't loop unless you make it loop, a "continue" continuation wouldn't really make any sense.)
Oh, I didn't realize comments were automatically disabled. It makes sense, though, I suppose.
Surely syntax highlighting requires building an editor.
I'm not sure if you're talking about a dialect-specific editor or an editor specific to Blade. Still, I'll assume you're not talking about a Blade-specific editor, 'cause Blade isn't very specific. :-p There can be Blade-specific tools, but the point is for the specification format to be completely flexible.
If you mean that every new dialect would surely need a new editor to accomplish its syntax highlighting, I doubt that. We already have plenty of editors which can be configured for syntax highlighting. I expect a few of them can be scripted so that a single command saves the project, builds it, updates the configuration files based on the result, then finally reloads them and refreshes the highlighting. I haven't looked into it, though.
In any case, I do expect some dialects to be crazy enough that they really do require all-new editors. I guess GUI widget libraries are the main culprit here. But for the most part, I expect dialects to be built based on whatever's easiest to edit (and process) to begin with. For a content type like vector graphics where a non-text editor is pretty much necessary, I expect existing editors like Inkscape to be used, even if that means railroading the dialect into being a specialization of SVG or whatever the editors don't complain about.
---
I guess this is as good a time as any to say that I'm kinda in a rut with Blade. I was making Turbine, an Arc-like sublanguage meant to be a good foundational Blade language, and I got bogged down implementing Turbine completely in continuation-passing style. (My initial "Who cares how much pain the core of the language is when there's so much syntactic potential!" chutzpah only took me so far. :-p ) When I get working on Blade again, I expect the first thing I'm going to do is to port it to Arc, and the second thing will be to write some much-missed macros.
That, and hopefully I'll work out a kink in Turbine's design. Rather than having s-expressions as syntax, Turbine is going to have its macros receive complete information about where their forms are located in the project, so that they can go parse their bodies themselves.
At about the same time I started my Blade hiatus (coincidence?), I realized that with this approach, you couldn't simply have one macro form expand into another macro form. For instance, if [unless x ...] expanded into [when [no x] ...], then the 'when form wouldn't actually be in the project!
I'm pretty sure the solution will be to allow for phantom documents that exist merely in the build and not in the project's file structure. However, I'm treating that idea very carefully 'cause I want to make sure it's still a sensible design when viewed from the perspective of someone who's using a debug stepper or who's looking up a code citation given in some logged error message--whether or not this person has some kind of Blade-aware IDE.
Right away, I have a couple of things that I'd sorta like to see implemented (if you don't mind). On the other hand, it might just be that you haven't gotten around to them yet.
1) Strings aren't escaped, which makes them pretty unhygienic (albeit technically livable).
"'"
''';
"\\'"
'\'';
This could be handled just by being more careful and running the string through a filter function before printing it out as compiled JavaScript. Note that the function would probably be defined within the scope of the private 'nest-lev variable so that it could put in the right number of backslashes.
2) Your 'compose isn't a metafn (and neither are 'andf and 'complement, but 'compose is the main one).
(do:a b)
(function(){var g5870=arraylist(arguments);return (function(){return apply(a,g5870);})['call'](this);})(b);
(do (a b))
(function(){return a(b);})['call'](this);
(a:do b)
(function(){var g5871=arraylist(arguments);return a(apply(do,g5871));})(b);
(a (do b))
a((function(){return b;})['call'](this));
This could probably be handled by putting yet another 'if case in 'js1. You could call out to metafn expansion utilities (like arc.arc's 'setforms does) or just hardwire the metafns into the 'if branch (like arc.scm's 'ac does).
Anyway, good stuff. ^_^ I especially like how arc.js is generated by js.arc. XD
Ack, there's something missing from that implementation: Nested string handling.
This is working well:
"\\"
'\\';
If you use 'quote to compile a string without escapes, you're fine:
'"foo"
'\'foo\'';
If you do the same thing where the only things to escape are quotes, you're fine, 'cause you call 'js-q, which uses 'nest-lev:
'"'"
'\'\\\'\'';
However, with other escaped things you're in trouble:
'"\\"
'\'\\\'';
I suggest putting 'js-charesc where 'js-q is now and having it use 'nest-lev directly. You might not even need 'js-q, since it could just be (js-charesc #\').
Right. I'm going to use [] as makeshift quotes. The JavaScript ['\'\\\\\''] evaluates to ['\\'], which evaluates to [\]. The JavaScript ['\'\\\''] evaluates to ['\'], which evaluates to an error, 'cause it's an unterminated string.
Could you help me to better understand the metafn issue? Those (do:a b) examples... what should they compile to? Does what you're talking about affect semantics, or just readability and performance?
When Arc compiles (a:b c), it compiles ((compose a b) c). When it compiles ((compose a b) c), it compiles (a (b c)). So in Arc, (do:a b) and (do (a b)) are basically equivalent, and so are (a:do b) and (a (do b)), where 'do is just an example macro. Since 'do is a macro, the equivalence does involve semantics; your transformation of (a:do b) calls a function named "do", when that function may not actually be defined.
Anyway, I'm just trying to make sure I inform you of stuff you may have overlooked. Once you know what I'm talking about regarding 'compose, it's up to you to decide whether you actually want to preserve an (a:b c)-(a (b c)) equivalence. ^_^ I am quite a fan of it, 'cause it saves some nontrivial edits (adding a closing parenthesis in a distant place and reindenting), but it's not sacred or anything.
Ah, I think I understand now. I had seen this comment from arc.arc:
; Composes in functional position are transformed away by ac.
And your examples show how 'compose gets transformed away, but I was having trouble visualizing a case where that wouldn't happen. Now it seems obvious to me: if you have (compose a b), rather than ((compose a b) c), then you can't transform compose away, because something is needed to make a and b act like one function.
; Short for "if both or neither and," 'ibona checks 'x and 'y against
; 'test (which is sent through 'testify). If one is truthy and the
; other is falsy, this results in nil. If they're both falsy, this
; results in an 'and of the 'ifneither and 'eitherway expressions. If
; they're both truthy, this results in an 'and of the 'eitherway
; expressions if they exist, and it propagates the result of calling
; the test on 'y if they don't.
(mac ibona (test x y ifneither . eitherway)
`(fn-ibona ,test ,x ,y
(fn () ,ifneither)
,(when eitherway `(fn () (and ,@eitherway))))))
(def fn-ibona (test x y ifneither (o eitherway))
(zap testify test)
(if do.test.x (aand do.test.y
(if eitherway (do.eitherway) it))
do.test.y (aand (ifneither)
(if eitherway (do.eitherway) it))))
(def congruent (x y)
(ibona atom x y
(and (congruent car.x car.y)
(congruent cdr.x cdr.y))))
(def congruent-sigs (x y)
(ibona atom x y
(ibona optional car.x car.y
(congruent-sigs car.x car.y)
(congruent-sigs cdr.x cdr.y))))
I've found myself doing...
(if atom.x atom.y
atom.y nil
...)
...a couple of times myself, so I wouldn't be surprised if (some variation of) 'ibona were surprisingly useful. :-p
EDIT: Hmm, this doesn't abbreviate as much, but its implementation and description are much more concise.
; This is similar to 'whenlet, but for binary if-and-only-if (i.e.
; xnor).
(mac xnor2let (var x y . body)
`(fn-xnor2let ,x ,y (fn (,var) ,@body)))
(def fn-xnor2let (x y body)
(only.body:.y:if x idfn no))
(def congruent (x y)
(xnor2let it atom.x atom.y
(or it
(and (congruent car.x car.y)
(congruent cdr.x cdr.y)))))
(def congruent-sigs (x y)
(xnor2let it atom.x atom.y
(or it
(xnor2let it (optional car.x) (optional car.y)
(and (or it (congruent-sigs car.x car.y))
(congruent-sigs cdr.x cdr.y))))))
Hmm, interesting. If I were opting for another control structure (which I probably wouldn't in this case, but hypothetically), I think I'd make your ibona closer to this (not tested):
(mac unless-both (test x y neither)
(w/uniq (f fx fy)
`(withs (,f (testify ,test) ,fx (,f ,x) ,fy (,f ,y))
(or (and ,fx ,fy)
(and (no ,fx)
(no ,fy)
,neither)))))
(def congruent (x y)
(unless-both atom x y
(and (congruent (car x) (car y))
(congruent (cdr x) (cdr y)))))
(def congruent-sigs (x y)
(unless-both atom x y
(and (unless-both optional (car x) (car y)
(congruent-sigs (car x) (car y)))
(congruent-sigs (cdr x) (cdr y)))))
I'm not sure if there's a better name than unless-both. Also, you could conceivably make neither a rest arg and just splice it into the and so you don't have to do (unless-both atom x y (and ...)). I just think it reads better with the explicit and. (Of course, adding a macro here is probably overkill anyway. Fun, though!)
Hmm... my code originally looked a lot like that (except for naming and the implementation of 'unless-both). Then I edited my post quite a bit because I thought there was a problem with the (and (unless-both ...) (congruent-sigs ...)) expression. (I can't remember what it was now, and I think I was mistaken.)
So I added the 'either-way parameter, and then I moved the logic into a function to make the evaluation order more to my liking--no calling the test on 'x before 'y is evaluated--and it got to the complicated state it's in now. In fact, I see some bugs now; the place where it says "do.test.y (aand (ifneither)" should be "do.test.y nil (aand (do.ifneither)".
For whatever it's worth, here's 'ibona again, with your much better name, and without 'eitherway. The only thing that's really different from your version is the argument evaluation timing.
(mac unless-both (test x y ifneither)
`(fn-unless-both ,test ,x ,y (fn () ,ifneither)))
(def fn-unless-both (test x y ifneither)
(zap testify test)
(if do.test.x do.test.y
do.test.y nil
(do.ifneither)))
(Before you get too excited, I'm not the person you replied to. ^_^ )
First, for your particular example, you could just do this:
a!b.4!c!d!e!f
If you need (a!b 4 5), it does get more complicated, and I've gotten a bit annoyed about that myself. Nevertheless, there's still a way (albeit a way which requires a bunch of typing to refactor into):
(!f:!e:!d:!c:a!b 4 5)
You know, I bet this would create an awful lot of JavaScript. XD
In practice, I rarely have more than three chained property accesses in C-like languages, or more than four things connected by ssyntax in Arc, so I think I'd just add one or two sets of parentheses and live with it. I won't pretend my case is typical, though. :-p
No, no, I trust that your translation is correct. I was just disappointed that it would compile down to this much JS code since my example was design to model a.b(4).c.d.e.f.
I don't have a running Arc to check it on at the moment because mzscheme 372 does not compile for me (probably my gcc version is too new).
Ah, I see. Yes, at the moment this compiler isn't very good at generating minimal JavaScript, since it's so faithful to arc.arc's macro definitions. A lot of the later work might involve optimizing it to produce smaller, more efficient JS.
Of course, you can still use (((((a!b 4) 'c) 'd) 'e) 'f) to generate a.b(4).c.d.e.f. [1]
> mzscheme 372 does not compile for me
Did you know Arc 3.1 works on the latest MzScheme? [2]
---
[1] Actually, you might be further disappointed to know (((((a!b 4) 'c) 'd) 'e) 'f) is currently compiling to:
get here is a JS function not unlike rocketnia's ref [3]. Its purpose is to disambiguate the Arc form (x y), which may compile to x(y), x[y] or (car (nthcdr y x)), depending on the type of x (function, array/object or cons, respectively).
I wrestled with this disambiguation problem for some time and finally settled (for now ;) on a simple inference system based on the most common use cases. The algorithm is:
1. If the form has a single quoted arg, as in (x 'y), it's compiled to x['y']. This allows object access chains like document!body!innerHTML to be compiled correctly by default.
2. If the form has 0 or 2+ args, or 1 arg that isn't quoted, then it's considered a function call:
(x) => x()
(x y) => x(y)
(x y z) => x(y,z)
I'm still looking into the least kludgy way to pass a single quoted arg to a function. Here are some options:
(x "y")
(x `y) ; quasiquote isn't currently used for anything else
(x 'y nil) ; the function can just ignore the nil arg
(fncall x 'y)
I don't know. If it comes up often enough, I think I'd rather have a special (fncall x 'y) ssyntax. Maybe x!y could expand to (fncall x 'y) and x.`y could expand to (x 'y).
I had assumed that since x.'y was read as two distinct symbols, x.`y would be too, but it's not the case:
arc> 'x.'y
x.
arc> y ; still evaluating previous expr
arc> 'x.`y
|x.`y|
Any idea why these are treated differently? Whatever the reason, it means I can use x.`y without hacking the reader. So, thanks for pointing this out to me! ^_^
I'm currently torn about whether to do
x!y => (x 'y) => (fncall x 'y) => x('y')
x.`y => (x `y) => (objref x 'y) => x['y']
as you suggested, or the reverse. Leaning toward your way so that functions are totally normal and objects special, rather than having functions with a single quoted arg be some exception.
This example works particularly well because the $("a") jQuery selector can be compiled from $!a. A challenge arises with more complex selectors, as in this snippet from the Find Me: Using Selectors and Events tutorial:
Since $("#ordered list") has the special character #, we're unable to compile it from $!#orderedlist. Either most of the ssyntax has to be sacrificed for parens, as in
Not quite sure (I suspect it's a bug), but it seems like it has to do with the implementation of make-readtable (which brackets.scm uses).
$ mzscheme
Welcome to MzScheme v4.2.1 [3m], Copyright (c) 2004-2009 PLT Scheme Inc.
> (parameterize ((current-readtable #f)) (read))
x`y ; read in as two items
x
> y
> (parameterize ((current-readtable (make-readtable #f))) (read))
x`y ; read in as one symbol
|x`y|
In fact arc3.1 even works on Racket, the new PLT Scheme. Only thing is that the command-line "racket" prints a newline after the "arc>" prompts, for some reason. But you can open as.scm with the editor DrRacket (as you could with DrScheme), set the language to be "Pretty Big", and hit Run; it will work.
For some reason, now I don't notice any issues with the "arc>" prompt in "racket" either. And I don't think I'm doing anything differently than I was before. ...I am forced to conclude that, when entering things into the REPL, I held down the return key long enough that it accepted an extra (blank) line of input. This explains the behavior exactly. Strange that I should have done this several times in a row... and how embarrassing. Oh well. At least now I can give racket a clean bill of health.
That is a known issue with Windows. (I'm guessing it's the reason arc3 is still the "official" version on the install page.) Simple workaround[1]: Find the line that says:
Could you talk about your decision to use it for Readwarp then? If Arc's not really ready for production use, might it still be a good choice for a certain minority of developers?
Yeah, I'm not trying to say you shouldn't use it for production use :)
They're opposing perspectives. As a user of arc I'd throw it into production[1]. At the same time, from PG's perspective I'd want to be conservative about calling it production ready.
I suspect arc will never go out of 'alpha' no matter how mature it gets, just because PG and RTM will not enjoy having to provide support, or having to maintain compatibility.
[1] With some caveats: treat it as a white box, be prepared to hack on its innards, be prepared to dive into scheme and the FFI. And if you're saving state in flat files, be prepared for pain when going from 1 servers to 2.
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
> 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:
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?
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.)
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.
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.
("foo" is really passed to a function document.getElementById.)
Right... but I wonder if you understand what I understand by that. If you allow for (document.getElementById "foo"), then document.getElementById will need to have a different meaning in functional position than in non-functional position, just like "foo.x()" and "(true && foo.x)()" have different meanings in JavaScript. (The first one uses foo as this in x, and the second one uses the window or whatever as this. The "foo.x()" form amounts to one syntax, even though the "foo.x" part can be wrapped in grouping parentheses without changing the meaning.)
The only example you give that doesn't require this kind of treatment, and in that way is the least misleading, is prefix, 'cause the \. form could be a simple macro.
On the other hand, the treatment isn't especially outlandish, 'cause Arc does the same thing; (a:b c) is different from ((and t a:b) c), thanks to the fact that a (compose ...) form is given special treatment in functional position (where it's the head of a metafn call). So with the right programmer-bewares in your documentation, you could use all-new ssyntax and metafn rules and mimic JavaScript's quirky invocation in a way that still works somewhat like Arc.
With that approach in mind, selected parts of the expansion might go like this (except probably depth-first, rather than breadth-first):
Note that Arc's 'get doesn't expand as a metafn that way; 'get only gets special treatment in setforms. Here, I've treated 'jsget so that "foo.a(1).b(2).c(3)" can be expressed as ((.c ((.b (foo.a 1)) 2)) 3), as opposed to something even more ridiculous. (Er, but (jscall (jscall (foo.a 1) "b" 2) "c" 3) is more readable IMO, and sooner or later you'll probably want to define a macro for (dots foo (a 1) (b 2) (c 3)) like the prefix option anyway, so maybe the whole 'jsget issue is pointless. :-p )
So yeah, I hope these complications help show you the way. If they lead you screaming in another direction, that's progress too, eh? ^_^