I Built a Lisp Compiler
mpov.timmorgan.org
mpov.timmorgan.org
Back in 2006 –when I was studying CS at Rollins College– I wrote a Scheme (subset of Lisp) interpreter that also shows a visual representation of linked lists and function calls. You can check it out here: http://davidpilo.com/pvts/
The interpreter was written in Java and while it does not support the full R5RS grammar it supports quite a bit of it (see http://davidpilo.com/pvts/language.html)
Cheers!
There are people who'd fight you over that one. :)
From Wiki: http://wiki.c2.com/?IsSchemeLisp
> One of the interesting debates in Lisp circles is the question of whether or not SchemeLanguage should be considered part of the LispLanguage family. There are more than a few Lispers (and perhaps a few schemers) who think that the LispSchemeDifferences are sufficiently large that Scheme should not be considered to be a "Lisp". This topic has spread across many pages, so this page has been created to quarantine this particular HolyWar.
And one interesting reason why not:
> The philosophies of the Lisp and Scheme communities have diverged quite a bit. SchemeLanguage focuses much more on FunctionalProgramming; CommonLisp on metaprogramming and multi-paradigm programming (especially OO with CommonLispObjectSystem).
MAL is easy, and straightforward - I'm not saying this to devalue your work (great job!) but to point out that the language is by design simple to implement and doesn't require any prior knowledge of how an interpreter works. People who want to write their own implementation should try that right now, and definitely not be scared off by "I spent 10 years working on this". Turning that interpeter into a compiler is an extra step on top.
Certainly, but the problem with implementing languages is the HUGE cliff on complexity after your first "calculator" or lisp.
You can make a simple interpreter in hours, even minutes. Then, suddenly, you get AMBITIOUS.
That is what take years!
P.D: I'm also in the hunt for a relational language (http://tablam.org), and I now in the what, 3 years of it? (Whoa! I search for my old mentions of it and the first I found is from 2013! https://www.clubdelphi.com/foros/showthread.php?t=84835&high...)...
This is a personal dream for me as well. The commenters in this thread (@gavinpc, @vidarh) will also agree with you: https://news.ycombinator.com/item?id=18634555
Anyway, if you wanna join the dream, I will not complain ;)
I’ve followed BuildYourOwnLisp[0] in the past, so it’s cool to see something that is more focused on the compilation of Lisp rather than its implementation as a language.
I needed a simple language as a vehicle for a compiler talk I'm preparing for this summer, so I hacked along an extremely reduced LISP (a 'Non-LISP', as I call it), whose C++ implementation from scratch came out at about 1K -- 1.2K LOC with the AST optimizations I meant to demonstrate for the talk. Self-contained code here -- no libs or (much) STL: https://github.com/blu/tinl
OP, thank you for sharing Tim Morgan's work -- it's a work of love!
You're aware of clasp?
https://github.com/clasp-developers/clasp
Which builds (in part) on embeddable common lisp:
https://gitlab.com/embeddable-common-lisp/ecl
See around 20-30 minutes: https://youtu.be/8X69_42Mj-g
I did come across some small LISP implementations at the early stages, but by that time I already had the AST builder done. Maybe because I didn't actively search for LISP implementations, as I didn't need a 'proper' LISP per se, more of a DSL for quickly writing ASTs of arbitrary complex computational expressions. Those ASTs were the final goal ; )
I wish I'd been given that advice before I wrote a compiler. Back then, the best advice was in Richard Bornat's Understanding and Writing Compilers and the dragon book - useful, but not a walkthrough.
There's a tool for this nowadays: `clang-format`.
There are some other features I wish it did for me in cleaning up my generated code. For example it doesn't remove superfluous parenthesis. It doesn't remove unused labels. It doesn't remove superfluous semi-colons (a single line of just a semi-colon). And so on. (I should, of course, just be building up an AST and pretty-printing the AST instead.)
The compiler is written in rust which generates C (that's currently dependent on being compiled by GCC or Clang). https://github.com/nitros12/some-scheme-compiler
>"I saw an x86 assembler written in Ruby which intrigued me, but the thought of working with assembly gave me pause."
Might anybody know the name of this Ruby-based assembler and/or where to find it?
But, to be fair, getting to the point of being able to implement a recursive fibonacci algorithm is not too difficult taking either path.
Using Forth effectively takes practice, but I found it worth the effort for the much the same reasons as Lisp.
Register machine languages are "catenative" also. We can take a "mov eax, ebx" and catenate that with "move ecx, $foo" in any order we want. We are constrained, though, because different sections of code use different input and output registers (or combinations of input and output registers and stack locations and such). Forth makes those conventions uniform, and that leads to some degrees of freedom which give it a higher level flavor.
The thing to do is to make some higher level language that uses Forth as a VM; then you have understandable code.
But, personally, I would really like to write a compiler for a Lisp specifically because I find the handling of macros really confusing and I don't think I'll understand it better until I actually do it.
If you boostrap a Lisp compiler by writing a Lisp interpreter first, with macros and all, then there is nothing left to do, macro-wise, when you set out to make the compiler.
You can freely use macros in that compiler's own source code, too.
There is an interaction between macros and compiling in the larger picture. When you have a working compiler and you integrate it into a file compiler, now your macros are executing at a bit of a different time: when your file compiler reads a file of code, it has to run the macros to expand it. Then when the compiled code is loaded, the macro calls no longer exist. The compiling happens in the development environment, whereas the loading may be in the deployed environment which may be a completely different system. So macros that assume they are executing in the production environment (due to being interpreted) are in for a rude surprise: they are now run in the build environment due to the shiny new compiler. That kind of thing can introduce "squirrely" issues.
Another squirrely issue is: what is a "top-level form". Suppose we have
(progn (defmacro foo ...) (foo ..))
if the (foo ...) call is to be able to refer to the definition, that definition must be evaluated so that it takes effect! But that means that evaluation must be interleaved with macro expansion to some extent; we can't just expand the whole (progn ...) all at once.The way out of this is to have requirements like: (1) any form not enclosed in another form is a top-level form and (2) if a (progn ...) is a top-level form, then its constituents are considered to be individual top-level forms, and (3) each top-level form is individually macro-expanded and compiled/evaluated.
One point I can think of is, in an interpreter you're presumably keeping track of program's vars in your own data structures instead of just throwing pointers around. Though, this might be unavoidable with a dynamic source language.
For example, if you had an index into a block of memory that you could prove was only large enough to address elements in the block, there is no need to check at runtime that it won’t be used to create out of range references for a compiler. If the same operation was done by an interpreter you might still check because the code that does the interpretation may not be solely used for that instance of the indexing/addressing operation, as you can guarantee with code produced by a compiler.
https://en.wikipedia.org/wiki/Partial_evaluation#Futamura_pr...
You'd link to libtcc.a and don't need to worry anymore