I have never used Swift, but I can confirm that writing a Lisp is a lot of fun in something like C or Go. I especially like the fact that you can build in so much raw power by implementing Lisp macros for example.
Yeah! Even more so if you have first-class macros (fexprs) like say Kernel does. I had a lot of fun building a fexpr-based lisp-1 with quasiquote (which Kernel eschews) and also with a first-class apply that works on both functions and macros. Not even Kernel can do that.
$ git clone https://github.com/akkartik/wart
$ cd wart
$ ./wart
ready! type in an expression, then hit enter twice. ctrl-d exits.
mac (foo x)
`(+ ,x 1)
=> (object function {sig, body})
(foo 3)
=> 4
(foo @(list 3)) # apply analogous to the ,@ splice operator
=> 4
(foo @'(3))
=> 4
Take it a step lower and do one in assembly and it is even more fun. I almost have enough code together to say that I'm doing one in RISC-V assembly. At this point I have already accidentally picked up a more intuitive understanding of how a lisp compiler works.
A similar project I've considered: write an Algol compiler in assembly. Algol is another elegant, influential language originally implemented in assembly.
I wrote a lisp in Go a while back and got to the point where it could parse and interpret basic lisp code. I got stuck on making it do proper tail call optimization since I didn't want it to do trampolining. Since Go doesn't support TCO the only way I came up with was to rewrite the interpreter either use a big while loop or gotos instead of relying on function calls. Does Swift do TCO making this trivial? Anyone else solved implementing a lisp with TCO in an elegant way in a non-TCO language?
One way is to model the call stack explicitly. I.e. you will need objects (or structs, in Go) representing a stack frame, and have a stack of those (the actual call stack). Evaluation works very different that way, since you can no longer piggyback on the implementation language's call stack. It does mean that you can now do the desired tail call optimizations, e.g. if you're evaluating (if cond a b), and you've evaluated 'cond', then you can replace the if's stack frame with one containing just a or b, and continue evaluation there.
One benefit of this is that continuations become trivial to implement (they're just a snapshot of the call stack at a given point).
Admittedly, almost all write-your-own-Lisp tutorials omit this, and use a bunch of recursive calls (in the implementation language) instead, which makes it very hard to do TCO, continuations or anything else that requires a real call stack (exception tracebacks, for example).
(I hope this answer makes sense; it's 3 in the morning here, so clarity might suffer a bit. ;-) But as it happens, I am writing a (hopefully non-toy) Lisp interpreter myself, in Go, so the question attracted my interest.
Author here, no, it doesn't support TCO and you'd have to resort to trampolining here too. In another post[1] I've talked about this and I agree it's definitely not an elegant workaround.
You can build a CEK machine (https://cs.umd.edu/class/summer2015/cmsc330/material/cek/), switching from big-step operational semantics to small-step operational semantics. Then you dump the CEK machine in a while loop that runs until the continuation is empty and the code is a value. (On a side note, this is how most C-targeting Scheme implementations work.)
Also, the type of NIL in Common Lisp is NULL. There is a NIL type, but it is empty: there exists no value which is an instance of the NIL type. NIL is a subtype of every type (including itself), and the type T is a supertype of every type, including itself. This situation with NIL and T on opposite ends of the "type spindle" is quite clever and subtly better than making NIL a type which contains NIL.
Can you suggest any example Common Lisp programs that print the _types_ NULL or NIL or T? Or show off the benefits of this spindle arrangement? I can intuitively see the benefits of this approach, but an example makes things much more memorable.
The nice thing about T is that it's the first letter of True and of Type. E.g.
;; catch-all method for any type
(defmethod foo ((arg t)) ...)
;;
(defmethod foo (arg ...) ...) ;; alternative spelling: omit class
NIL is the empty list and that is mnemonic for "empty type". For instance, the intersection of two mutually exclusive types is the empty type. Nothing can be both a cons and an atom, so the type denoted by the type expression (and cons atom) is empty; and that is nil. The set manipulation which underlies types needs an empty set, and nil fill the need for that empty set (pun intended).
NULL is useful in writing methods that capture NIL (the only instance of NULL). That may be recognized as the "null object pattern":
(defmethod foo ((arg null))
;; (foo nil) called
)
(defmethod foo ((arg number))
;; (foo <number>) called
)
(defmethod foo ((arg t))
;; (foo <anything else>) called
)
This is not quite precisely true. Everything is a generalized boolean, but not a boolean. Only T and NIL are of type BOOLEAN, which you can check with the function:
https://github.com/kanaka/mal
I find it to be a great tool for both learning about Lisp and the language you're writing your mal implementation in.