Hacker News new | ask | show | jobs
by reikonomusha 2480 days ago
This was an incredibly enjoyable read. A lesson to take away is that many of the ideas of Lisp can be taken advantage of without reeling in the entirety of an existing stack.

Writing a Lisp parser is easy. Walking Lisp code is easy. Serializing Lisp code is easy. Adding a new primitive is easy. Adding very basic syntax transforming macros is easy. All of these are virtually trivial if your host language is a Lisp, as was the case with Co2.

What they didn’t do is what many people might think are table stakes with Lisp: writing a garbage collector, writing a runtime, supporting lambdas, and so on. Those are unreasonable asks for 2K RAM on a 6502. I wouldn’t say they wrote a bonafide Lisp, but they made use of many ideas of Lisp successfully to write a game that is very surprisingly readable while not being too abstract over assembly.

Since Lisp began in the 1950s, it has always needed to stay tied to the low level. Even today, with SBCL or CCL, you can write your own assembly code. One relevant thing to the article is Baker’s COMFY 6502 language for writing assembly code [0]. A few implementations can be found on GitHub.

[0] http://home.pipeline.com/~hbaker1/sigplannotices/sigcol04.pd...

3 comments

If you aren't going to have a GC, "rich" runtime or lambda support, what does LISP really bring you over FORTH? And implementations of the latter on 6502 have been commonplace since the 1980s...
I understand that Forth is powerful and elegant, and one of the last languages I'd want to take on in a fight when wielded by a master, so let me pretend you were asking about a stripped-down Lisp compared to, say, Pascal, instead:

* The simple syntax of stripped-down Lisp is very amenable to application-specific or domain-specific macros. This turns out to be a convenient way to do things that often the language or compiler alone can't do as well, if you used only functions, data, and conventions.

* That the syntax is so simple, and can also be first-class data that is displayed from the programming environment like it looks in syntax, makes it especially nice for things like intermediate representations that are refined incrementally. For example, you can show a translation series of steps that go from syntax parse, to resolutions, to phases of optimizations, to high-level assembler, to a very low-level target code (still represented with parentheses and "opcodes"), from which you write bytes. This can also be convenient.

In this particular case, they're using Racket (an implementation of a dialect of Scheme) to implement a compiler for a Lisp dialect they invented. Using Racket gives them both a nice general-purpose language for implementing their compiler, and happens to already have a lot of tools for parsing their own stripped-down Lisp and manipulating it.

IIUC, Naughty Dog used Racket for a similar purpose: to implement their own Lisp. For a narrative DSL for some AAA titles.

Going back a few more years as many of you know, Naughty Dog used a LISP like called GOOL on Crash Bandicoot on PS1.

https://all-things-andy-gavin.com/2011/03/12/making-crash-ba...

And later a follow up language GOAL for PS2 https://en.wikipedia.org/wiki/Game_Oriented_Assembly_Lisp

Naughty Dog used Allegro Common Lisp by (still alive and well) Franz Inc.
Neat, I didn't know Naughty Dog also used Allegro CL, in addition to Racket:

* "RacketCon 2013: Dan Liebgold - Racket on the Playstation 3? It's Not What you Think!" https://www.youtube.com/watch?v=oSmqbnhHp1c

* "Functional mzScheme DSLs in Game Development" (MzScheme was the name of the main PLT Scheme interpreter that was renamed Racket) http://cufp.org/conference/sessions/2011/functional-mzscheme...

Hard reasons.

Forth implementations on the 6502 uses a stack and require more RAM than Co2 which uses a compiled stack.

I feel like Co2 would compile to faster code but of course I haven't benchmarked this. The reason I feel this is so is that I way I understand compiled forth is that the 'words' are still threaded so you don't escape the interpreter overhead.

Soft reasons.

Co2 takes away the chore of parsing, in Forth you are the parser. It's simpler but more error prone and harder to read (subjectively).

Forth is postfix notation which for a lot of people is challenging.

> Forth implementations on the 6502 uses a stack and require more RAM than Co2 which uses a compiled stack.

I don't know what you mean when you say this. Could you elaborate? Any function call is going to require putting the arguments somewhere.

> The reason I feel this is so is that I way I understand compiled forth is that the 'words' are still threaded so you don't escape the interpreter overhead.

Indirect/direct threading at runtime isn't required; it's up to the compiler. There are plenty of Forth cross-compilers (e.g. MPE's) that compile native code (and call it "subroutine threaded"). They don't have an explicit (inner) interpreter... they just use standard CPU opcodes to call/return.

> I don't know what you mean when you say this. Could you elaborate? Any function call is going to require putting the arguments somewhere.

There's a footnote in the article that might be helpful:

> this is thanks to a "compiled stack", a concept that's used in embedded programming, though I had a hard time finding much literature about it. In short, build the entire call graph of your project, sort from leaf nodes to roots, assign to each node memory equal to it's needs + the max(children)

Not having to think about a stack all the time.
They used Lisp to generate machine code. They didn't parse lisp at all. Imagine writing a library in your favorite language to do what they've done.
They did parse it, albeit indirectly, by Racket’s reader. Co2 is a language, not a bunch of function calls, so it’s not quite the same as building a library in your favorite language. The article even gives examples of new syntax they produced.

Parsing Lisp in Lisp is so easy because it’s free.

you just need to implement read-syntax ...
I wasn't being sarcastic, it was very simple the way they've done it. https://docs.racket-lang.org/reference/Reading.html#%28def._...
I'm saying that there is a larger machinery behind it. It just looks simple.
> Parsing Lisp in Lisp is so easy because it’s free.

You just have to implement an s-expression reader.

Plus an interpreter, compiler or a code walker, which can actually parse the Lisp code.

> Parsing Lisp in Lisp is so easy because it’s free.

Are there any other languages that have this feature? I.E. where the data and the code are the same syntax?

Well, if there is new such language then it would be eventually called a lisp dialect.
Rebol springs to mind.
I think this is very true. It also wouldn't be hard to implement basic function pointers, lambdas that don't capture anything.