Bone Lisp – Lisp Without Garbage Collection
github.com
github.com
It being my first ever "real" c program, I'm not quite there yet. Very curious to read more source code of small languages, preferably things that are implemented in one file only.
[1] http://peter.michaux.ca/articles/scheme-from-scratch-introdu...
A Racket client for the Arduino Service Interface Protocol (ASIP)
FiDo, a tiny JIT compiler: https://github.com/JohnEarnest/Mako/blob/master/demos/FiDo.f...
A Logo interpreter: https://github.com/JohnEarnest/Mako/blob/master/demos/Loko/L...
A garbage collector used by a number of games and programs: https://github.com/JohnEarnest/Mako/blob/master/lib/Algorith...
A custom systems programming language targeting the VM (Java) https://github.com/JohnEarnest/Mako/tree/master/tools/Stroye...
http://johnearnest.github.io/Mako.js/?rom=Loko
Unfortunately some browsers have decided that backspace should navigate back, which makes typing at the Loko REPL rather awkward.
https://github.com/munificent/mark-sweep
And an article I wrote about it:
http://journal.stuffwithstuff.com/2013/12/08/babys-first-gar...
This was the inspiration for uLisp's GC (http://www.ulisp.com/show?1AWG). :)
If you want a more full-featured but still small language in C, you could take a look at Wren:
http://www.amazon.com/Compiling-Continuations-Andrew-W-Appel...
and Modern Compiler Implementation in ML:
https://www.cs.princeton.edu/~appel/modern/ml/
There's a certain kind of equivalence between continuations and SSA. Mostly, I bring that up because if you go down the rabbit hole of designing a compiler in C, you'll find talk of SSA, but continuations are somewhat advantageous for designing functional compilers. If you're just looking to design an interpreter, Ben Pierce's book Types and Programming Languages does a good job at showing how to put together a simple functional language both theoretically and practically:
Later versions include GC, and bytecode compiler + VM.
Edit: I highly recommend SICP (dense, but elegant), and Matt Might's blog as resources.
(Not that writing a lisp is a bad way to learn C of course!)
[1] "A Modern Implementation of the Common Lisp LOOP Macro", European Lisp Symposium https://www.youtube.com/watch?v=ZJr81DtSwUc
We might be at the point where this is an acceptable simplification for very fine grained processes and services. Has anyone explored this model?
However, the paradigm you're referring to is interesting. Short-living processes that allocate little memory could work. Also, servers could use this model: create a slab of memory for each request, and allocate memory on that slab when processing the request. When the request is completed, you can free the entire slab at once. I know the Ur language does something like this.
It uses explicit regions instead of garbage collection.
Explicit regions are both very simple and very fast, but how far can one get with them? I want to find out, so I am developing this interpreter.
So we've come full circle.
All research of Interlisp-D, Smalltalk and Mesa/Cedar at Xerox PARC:
https://archive.org/details/bitsavers_xerox
Stéphane Ducasse also maintains the original Smalltalk manuals on his legacy books:
http://stephane.ducasse.free.fr/FreeBooks.html
Oberon at ETHZ:
http://www.ethoberon.ethz.ch/books.html
If you want to see how the latest incarnation of Oberon used to look like:
http://www.progtools.org/article.php?name=oberon§ion=com...
For Modula-3 it is a bit harder, because of DEC/Olivetti being acquired by Compaq, which was then acquired by HP. So lots of sites are full with broken links.
So the older books are a better source than the Internet.
But still you can get some info here:
http://ftp.labs.hp.com/ftp/pub/dec/SRC/
http://www-spin.cs.washington.edu/
Burroughs overview:
http://www.smecc.org/The%20Architecture%20%20of%20the%20Burr...
It lives on as Unisys MCP mainframes. There are some more informations when searching for it.
Algol-68RS used at Royal Navy:
https://en.wikipedia.org/wiki/ALGOL_68RS
This is just a small taste. If one cares about history of computing there are lots of other resources scattered around the web and libraries.
"DMD does memory allocation in a bit of a sneaky way. Since compilers are short-lived programs, and speed is of the essence, DMD just mallocs away, and never frees. This eliminates the scaffolding and complexity of figuring out who owns the memory and when it should be released. (It has the downside of consuming all the resources of your machine if the module being compiled is big enough.)"
http://www.drdobbs.com/cpp/increasing-compiler-speed-by-over...
The downside to this is it makes it difficult to use tools like Valgrind that trace memory allocation in one's own program.
Maybe I'm in minority but in C I tend to flag unnecessary free()'s before program exit in code reviews (There is not much that you can do about that in "safe" languages like C++).
I'd just mention it makes tracking memory allocation harder for an end-user of an application framework that does. In those instances, it might be nice to, say, disable this approach with compiler flag or something.
Edit: it also depends on how concerned you are with memory leaks. Making sure every single allocation has a delete attached is a simple rule. The rule of "every allocation has a delete, except the allocations we're sure will last for the life of the program", making sure that rule is followed seems a little harder and presents a "mental overhead". This kind of thing probably depends on how prone to memory leaks your program is (I've seen big, messy, memory leak prone programs where "oh this one isn't deleted 'cause of optimization" would make the leak-detective's job just that much harder).
After all, the essential rule behind garbage collection is that all variables are available to a program for ever - yet the implementation is allowed to clear up memory if it can prove that a value can never be retrieved again (for instance, because all references to it have gone out of scope).
It has also crossed my mind that a large number of small, terminating programs might well be better off running to completion without any collection taking place. But language implementors are pretty smart - probably quite a lot of the small programs you have in mind already /do/ run right through without ever doing any garbage collection.
The main issue is that in most cases the complexity increase is not worth the trouble.
So such languages end up being used in embedded systems and high integrity deployments. Usually where human lifes are at risk.
On the other hand, generational GC is almost trivial to implement for immutable languages, exactly because there is no need for write-barrier. BEAM's GC is perfect example of how trivial can well performing GC be in that case (and good study material that is perfectly relevant for runtimes with mutable objects).
The mode of not freeing has been explored in programs that are short-lived and run on top of an OS that reliably cleans up memory. For instance, system utilities and compilers and such.
Got a C program that crashes due to premature allocation, according to Valgrind, and it is hard to figure out why? It it just a short-lived utility that does some job and exits? Then just #define free(p) ((void) 0) and rebuild.
Actually, D's compiler more or less works this way: http://www.drdobbs.com/cpp/increasing-compiler-speed-by-over...
http://lists.gnu.org/archive/html/coreutils/2014-08/msg00012...
[fork, allocate, exit] is a very valid and robust pattern leveraging the OS as a garbage collector.
This kind of pattern reminds me of crash-as-an-operation-mode software.
Also, some hapless intern is going to copy a chunk of code out of your project in their new project and then give you hell because their program leaks memory like crazy.
Also, some mid level manager is going to decide that all code must be run through static analysis and come out cleanly and you're going to have a bad day.
Given this setup I can prototype programs without thinking about memory at all. When my program runs out of memory I start measuring allocations and insert the odd pointer clear at strategic places until I don't, then I continue on my merry way. 99.9% of programs we write don't require thinking of "no memory leaks" as a black-and-white cast-iron guarantee.
This setup also provides a really nice teaching experience where students can learn about pointers and write all sorts of programs without ever running into weird bugs due to use-after-free or overflowing bounds. However it adds some overhead akin to a realtime GC: everytime you copy a struct you have to increment refcounts of any pointers nested arbitrarily deep within it (Mu maintains a flattened bitmap for each type to make this not utterly suck). Sum types get even crazier, because you might have a pointer at offset x depending on the state of the tag at offset y, and so on. I'll try to measure the overhead at some point..
Elegant! It's funny though, on persistent storage we sort of invented this decades ago in the form of hard links. You can have as many hard links as you want to a file, and the file will only be deleted when all links are removed and all file handles are closed.
dynamic memory allocation is frowned upon... to many issues with fragmentation and non-deterministic behaviour.
Tolpin and Toft designed an extension to the functional language ML which used region based memory managed instead of traditional garbage collection for ML.
This lifted the lifetimes of variables into ML's type system (!!!) while the underlying implementation IIRC could achieve O(1) memory behaviour except when an exception occurred.
While this sounds amazing, there were draw-backs on the implementation / theory as certain optimisations were near necessary to get good performance. I.E. word sized integers had to live in the heap as opposed to registers. Another issue was that loops had to be restructured from idiomatic ML style to a slightly different one, other the region inference logic would cause O(N) allocations in a loop which would otherwise use O(1) allocations.
http://www.elsman.com/pdf/retro.pdf
or "Tauplin and Toft region based memory management retrospective" should lead you to the paper.
In general copy-on-write schemes would be useful for this, and some persistent data structures can be as well.
JonL White wrote a paper about this idea in 1980: http://3e8.org/pub/scheme/doc/lisp-pointers/v1i3/p17-white.p... (favorite quote: "GC Once a Year: Enough?")
Rivest and Shamir wrote an interesting paper on write once memory in 1982 that I feel might have some non-obvious applications to this: https://people.csail.mit.edu/rivest/RivestShamir-HowToReuseA...
Linear types (http://home.pipeline.com/~hbaker1/LinearLisp.html) is a closely related idea in terms of avoiding garbage collection that has very useful concurrency properties. Most time-of-check-to-time-of-use and kernel/userspace race conditions would be prevented by using linear types for system call arguments. IMO linear types have enough benefits outside of memory management to make the trade-off of using them worth it vs the other techniques mentioned above.
Speaking of safe software, I came across a free chapter/snippet on memory and safety in Ada[1]:
"(...)Restrictions
There is a general mechanism for ensuring that we do not use certain features of the language and that is the pragma Restrictions. Thus if we write
pragma Restrictions(No_Dependence =>
Unchecked_Deallocation);
then we are asserting that the program does not use
Unchecked_Deallocation at all – the compiler will reject the program if this is not true.There are over forty such restrictions in Ada 2005 which can be used to give assurance about various aspects of the program. Many are rather specialized and relate to multitasking programs. Others which concern storage generally and are thus relevant to this chapter are
pragma Restrictions(No_Allocators);
pragma Restrictions(No_Implicit_Heap_Allocations);
The first completely prevents the use of the allocator
new as in new Cell'( ... ) and thus all explicit use of the heap. Just occasionally some implementations might use the heap temporarily for objects in certain awkward circumstances. This is rare and can be prevented by the second pragma.Hint: Ada is also a high level language that allows low-level programming, and might be fun for writing a lisp ;-)
[1] http://www.adacore.com/adaanswers/gems/gem-43-safe-and-secur...
(http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.39....)
If, for example, I want to put a value into the middle of a list, would I be doing something akin to. . .
MyList = MyList.firstHalf + NewValue + MyList.secondHalf?
And how doesn't this become a horrible bottleneck?
Read Chris Okasaki, "Purely functional data structures" for better options.
You can learn more in Okasaki's great book: http://www.amazon.com/Purely-Functional-Structures-Chris-Oka...
Your example is not possible in an immutable list. Also note that this reassigning of variables is a red flag even in standard Lisps. In fact, variables are not that common. The most common way of referencing something for later usage is the let form, which already creates new "variables" anyway. Just eliminate ways to reassign them (setf?).
What you would do is to create a new list containing the elements you want. You may or may not want to give it a new name afterwards. Most likely, you'll use as it is and pass on to some other function.
I can't comment on this particular implementation. But it does not necessarily have to be a bottleneck. In fact, by having immutable data structures, that means that you can share data like crazy, without fear of anything being replaced where it should not. All sorts of optimizations are enabled that wouldn't be possible otherwise. If this implementation uses them, it's another matter entirely. No idea.
Given a zipper (as x bs) you can move the cursor right (rather, compute the zipper with the cursor one step further to the right, since we're purely functional) as ((cons x as) (car bs) (cdr bs)). You can insert a new element "y" into you current position by computing (as y (cons x bs)). If you discard the old version of the zipper every time you move or insert, this will use the same amount of memory as just storing the list, and you get insertions in O(N) time and O(1) memory. There's a whole field devoted to making data structures like this for purely functional languages, and this zipper concept extends itself rather naturally to trees.
[1]https://en.wikipedia.org/wiki/Zipper_%28data_structure%29
That failure might prove that those ideas were bad. But I think it just proves that MS-DOS compatibility is really, really important.
Weep for humanity.
How mankind would be if there weren't a few lunatics that kept on trying to fly and fail?
Just to cite one example from many.
I am curious as well though. Maybe you could show an example?
allOnes = 1 : allOnes struct node { int x; struct node *next; } allOnesList = { 1, &allOnesList }, *allOnes = &allOnesList;In fact, with more thought, I realized the lazy evaluation is actually orthogonal to forming cyclic data. The "allOnes = 1 : allOnes" line doesn't necessarily form a cycle (depending on the interpreter/compiler, I imagine ghc and any other sensible implementation will form a cycle). Naively, this will just create a thunk which will get repeatedly evaluated, generating a long list of ones.
EDIT: Nevermind, you're right.
This observation is somewhat relevant to the weird semantics of "lexical" scoping in Python, because the obvious semantics would cause cycle between frame object and function defined in it's scope.
Obviously you can't trigger full GC in your GC code. So you have to write GC code in such a way that it won't trigger GC or, in some cases, very limited mode of GC that you know is safe to perform at that point. It is trickier in languages that implicitly allocates. Traditionally, Lisp programmers are pretty aware of what operation would allocate and good at avoiding them. Plus, an implementation often provide a primitive to turn off GC in certain regions. Another approach is to design a subset of language with guaranteed safety properties and implement the core part of GC with it.