Lisp in fewer than 200 lines of C
carld.github.io
carld.github.io
The post identifies some of it's own weaknesses (memory handling, errors), which are quite C specific. Or at least easier to handle in other languages, where you can punt those issues to the host language runtime. But it will be a fun extension to fix them (a job for the second evening / weekend of coding ;) )
But, imho, the beauty of writing a Lisp is that there are a bunch of things you can do from there, some more difficult, but several are achievable step-by-step in a day or a few days each. I'd first add a few special forms more than the OP (quote, if, progn, math operations), then my suggestions:
1. Defining names (if you haven't already), both let and define special forms.
2. Lambdas.
3. Tail call optimisation (I suggest this not because it's an optimisation, this Lisp doesn't need optimising, but because TCO is a bite-sized extension.)
4. Lexical scoping of lambdas.
5. Continuations. call/cc
6. And if you're really brave (or skilled, or just masochistic), macros.
I was encouraged to do this as a new grad student, and it was one of the most fun and educational experiences I remember. I didn't get as far as macros back then, but implementing call/cc was a definite pivot point in my programming competence.
Just be careful about scope creep. I started one as a weekend project five years ago and I'm still not finished ;)
A solution is needed if you want lazy evaluation.
No it's not about interpreted or compiled. It's about using the native stack or a heap for stack frames. Compiled code and interpreters can both use either, so that's orthogonal.
In addition to return address and dynamic link, you need to store a static link in each stack frame.
I'm writing Scheme R5RS in Kotlin (https://github.com/kovrik/scheme-in-kotlin) and have implemented everything except macros (6).
Have no idea how to beat them.
But then there are things like hygiene, performance and some tricky edge-cases.
And I couldn't find any standard (and simple) algorithm to implement macros (preferably written in something other than Scheme itself).
Still trying to wrap my head around.
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.464...
Check out John Shutt's thesis for more information on hygenic fexprs: https://web.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unr...
Anyone looking for a more complete implementation, check out klisp [http://klisp.org]. It's not 100%, but has some stuff not included in the Kernel report, some of it lifted from R6RS. The main thing it's missing which it desperately needs is an FFI.
I got something working in 65 lines [1]
[0] http://norvig.com/lispy.html
[1] https://gist.github.com/jmikkola/b7c6c644dff1c07891c698f0a52...
Also, another, more interesting Lisp by the same author: (http://piumarta.com/software/maru/). Maru is basically a lisp where some of the core functions like eval and apply are extensible from user code. There's basically a global map of types to evaluators and applicators, with some functions for you to register your own types and get the evaluation behavior you want.
Yes, jonesforth definitely inspired and influenced me; that's why Jones is first in the Acknowledgements section.
If you're curious, I keep a list of single-file implementations of programming languages (including jonesforth):
and their opinion was that it was easier to do the lisp than it was to do the FORTH. Although right now they are trying to improve their C Compiler prototype https://github.com/oriansj/M2-Planet before they convert it to assembly
If you were to implement the same Lisp in C then compare, then maybe the assembly variant would be faster for the reasons you mention. Or maybe not.
Also, I modeled arpilisp after the original Lisp. That's barely a first step, and possibly the wrong first step, for anything non-trivial, including applications requiring a "super fast lisp that can compete with Go."
But I didn't write arpilisp for performance. I wrote it to learn and share. Enjoy!
#define is_space(x) (x == ' ' || x == '\n')
#define is_parens(x) (x == '(' || x == ')')
Should be #define is_space(x) ((x) == ' ' || (x) == '\n')
#define is_parens(x) ((x) == '(' || (x) == ')')
Probably doesn’t matter in practice for this. It could end up being a nasty source of bug later on in the project. #define is_space(x) ({ typeof(x) y = x; y == ' ' || y = '\n'; })
(Or in this case, turn it into an actual function and let the compiler figure out optimization.) template<class T>
bool is_space(const T & x) {
return x == ‘ ‘ || x == ‘\n’;
} template<class T>
constexpr bool is_space(const T & x) {
return x == ' ' || x == '\n';
}
Debuggable, type safe and same performance as straight C code.Yep.
Then again, inline does not mean what most people think.
Also, the talk about pointers being aligned to "8 bit boundaries" I think means 8 byte boundaries. Memory is not bit-addressable (at least, not in C, on anything popular).
But I don't mean to detract from the project! It is very cool nonetheless :)
http://www.flownet.com/ron/lisp/l.py
The interpreter itself is 48 lines.
What's the reason for using macros instead of real functions? Is this an optimization because macros get inlined at compile time? Does this really bring a lot of value?
In this particular case, none whatsoever. It’s egregious abuse of macros.
EDIT: And gettoken() should check against buffer: index < sizeof token.
EDIT 2: And I'd store the tag in a separate variable, because bit abuse in a pointer is plain and simply asking for problems.
[0] https://github.com/kanaka/mal/blob/master/process/guide.md
Your opinion is welcome. Alain Marty
if (is_pair(cdr(ob))) {
printf(" ");
print_obj(cdr(ob), 0);
}
How could this `if` statement ever evaluate to false? We already verified that `cdr(ob) != 0`, and the CDR can never be a plain old string, so isn't this `if` superfluous?> a program with missing or unmatched parenthesis, unresolved symbols, etc will likely just result in something like a segmentation fault.
I understand this is just a fun thing to hack on, but this is an irresponsible way to write software. I hope no one here is reading this and thinking it's how they should be writing C.
Below is some of the relevant code in C/C++ from the virtual machine of Emblem (the Lisp dialect I'm using to implement inter alia my visual dataflow language, Full Metal Jacket). To keep things short I haven't included initialization or the garbage collector. cons_op takes the top two stack entries (one on the stack, the other in tos), conses them, and returns it in tos.
-------------------
typedef struct ListStruct {
void *head;
void *tail;
} *List;
static struct ListStruct consTable[NUM_CONSES];
static List freeStore;
#define hd(X) (((List)(X))->head)
#define tl(X) (((List)(X))->tail)
#define NIL (void *)&consTable[0]
#define null(X) ((X) == NIL)
static void **stack;
static void *tos; /* Top of stack register. */
static long sp;
static void cons_op()
{
List x;
if (null(freeStore)) { cons_gc(); }
x = freeStore;
freeStore = (List)tl(freeStore);
hd(x) = stack[sp++];
tl(x) = tos;
tos = x;
}
----------------------Alternatively, instead of C you could use a garbage collected language such as Java, C#, or Go, and then not have to worry about memory management.
So why is it fair to label it irresponsible?
Seems we disagree on the basic premise then. If someone learns the wrong thing based on my example I view myself as responsible. (I have not been a perfect example all the time either. We owe it to ourselves and others to always improve on that front.)
Yes, his C code could be a lot better, but at the end of the day it really doesn't matter and nobody is going to be using this for anything important.
Segfault is a totally safe way to terminate a process on a modern desktop os. Besides - there is no 'correct' way to write software.
In any case it's a bad issue and should not be a normal failure mode for a syntax error.
None of which matter if it's a prototype, running on a developers machine.
If you want memory safety you run the program through valgrind anyway with a large input dataset. And write unit tests. And integration tests. And so on.
The baggage of production quality software development environment is so high it easily stifles the joy of quick and dirty prototypes.
The main use of prototype is to facilitate understanding. This is the most critical constraint, whose needs drive over anything else.
Besides, who on earth is going to exploit a few hundreds of lines of code a developer runs on his or her own machine?
FWIW, I didn't downvote you, as I don't downvote someone simply because I believe they're wrong, or because I disagree with them.
Insisting that learners/students should only ever read and study complete, perfect implementations is, I think, a mistake. I've learned a great deal from studying, and subsequently improving and extending, implementations that are imperfect and incomplete.