I think that you can write a reasonable lisp entirely without quasiquote.
There are three pieces that you need:
- Functions can appear literally in the AST (à la Janet and Common Lisp)
- Macros can appear literally in the AST, and specifically they have to have a different runtime representation than functions
- Special forms are their own thing explained below
Basically when you're writing Janet-style macros, you can/should unquote pretty much every symbol in order to do lexical lookup at the macro-definition at compile time, rather than the call-site at runtime:
~(,+ 1 2)
And you could just write that as a list literal (in Janet syntax):
[+ 1 2]
Or, without square brackets, using a macro that does the equivalent thing:
(ast (+ 1 2))
Which would return a list of three elements: a function followed by two numbers.
But in Janet, you can't unquote macros, because macros are actually just functions at runtime -- macro-ness is a property of the binding, not of the value:
~(,for i 0 10
(,print i))
That would be an error; you need to keep for
quoted in order for it to expand as a macro. This means it's kind of awkward to write this as a literal:
['for 'i 0 10
[print 'i]]
Also it's awkward to quote i
like that. But I claim that you probably shouldn't be putting literal symbols into a macro expansions even though it's safe in this case -- so the macro would reasonably look something like:
(let [$i (gensym)]
['for $i 0 10
[print i]])
If you have a different runtime representation of macros (e.g. https://github.com/ianthehenry/macaroni), then you could just write this as:
(let [$i (gensym)]
[for $i 0 10
[print $i]])
Or:
(let [$i (gensym)]
(ast
(for $i 0 10
(print $i))))
Which is probably common enough to deserve its own shorthand:
(ast $i
(for $i 0 10
(print $i)))
Or something.
The last problem is special forms, which in Janet are special-cased symbols. while
is a special form in Janet, so you don't write:
~(,while true (,print "hi"))
Instead, you write:
~(while true (,print "hi"))
And:
['while true [print "hi"]]
Is awkward looking.
But you can get around this by making special forms not actually special-cased symbols, but instead their own types. Then you bind the symbol while
to the special-form <special:while>
in the default environment. Which means that you could write:
[while true [print "hi"]]
In order to get the list:
[<special:while> true [<function:print> "hi"]]
(Aside: you don't actually need a separate type for this; you can just (def while 'while)
in Janet today and write this just fine. But I've never liked special-casing certain symbols like this anyway, and if you have the separate type then you can make the evaluation rules for all symbols consistent: look up the definition of the symbol while
, see that it is bound to a special <while>
token, and then evaluate the <while>
special form.)
So now there's nothing that actually needs to be quoted in an AST. You can construct an AST using bracket notation instead of quasiquote. Or, because brackets are harder to read, using a macro that basically just inserts list
at the front of every paren:
(ast (while true (print hi)))
Becomes:
[while true [print hi]]
Or, in a language without list literals:
(list while true (list print hi))
You also then don't need unquote-splice, because you can just use the regular splice
that is already part of at least Janet:
Quasiquote style (unquote-splice):
~(while true ,;body)
Bracketed list style (just splice):
[while true ;body]
Bracketless list style (just splice):
(ast (while true ;body))
In Janet you use quasiquote when writing PEGs, which are symbolic expressions very similar to Janet ASTs, except that you really do want symbols to remain symbols. Those would be awkward to express without quasiquote. But of course you can imagine a different API for PEGs, or other symbolic expression data structures.