An experimental, small, readable lisp with thorough unit tests and extensible functions/macros.
Fetching latest commit…
Cannot retrieve the latest commit at this time
If I look at any small part of it, I can see what is going on -- I don't need to refer to other parts to understand what something is doing. If I look at any large part in overview, I can see what is going on -- I don't need to know all the details to get it. Every level of detail is as locally coherent and as well thought-out as any other level. -- Richard Gabriel, The Quality Without A Name (http://dreamsongs.com/Files/PatternsOfSoftware.pdf, page 42) Wart is a small, *super* readable, thoroughly unit-tested lisp. Wart returns to the roots of lisp: no constraints, maximum flexibility, extreme late-binding. Wart is not a platform. It exposes no interface, releases no version numbers. Anything can change at any time. If you use it, write lots of tests. Or God help you. Everything is open to question -- but you'll have to pry macros out of my cold dead hands. Wart is intended above all to be read. If you write programs using it, put them in this directory. Don't hide wart away somewhere in your path. Feel free to make changes to the language. You know your needs best. Wart will eventually be 'fast enough'. Right now it's 3-5 orders of magnitude too slow. It will always be small. 10-20kLoC should provide a useable foundation for 'real' apps. --- Code sample: def fact(n) if (iso n 0) 1 (* n (fact (- n 1))) ; Alternatively def fact(n) (* n (fact (- n 1))) def fact(n) :case (iso n 0) 1 All functions are generic and can be extended or overridden at any time. The implementation uniformly adds features to language primitives by extending them in this manner. def len(x) :case (isa x queue) (queue-length x) Other features: first-class macros (fexprs) that are also open to extension, pervasive python-style keyword args, and it can deduce parens from indentation (but traditional lisp-with-parens will always work). You'll need linux and gcc. To run it: $ wart wart> Hit <enter> twice to eval. Hit ctrl-d to quit. To run tests: $ wart test $ # success Wart started out as a Common Lisp implementation, which may be an easier read if you aren't fluent with C: http://github.com/akkartik/wart/tree/sbcl --- Directory organization Wart loads all files that start with a digit and have a '.cc' or '.wart' or '.test' extension. Files are loaded in ascii order. To create a new app, just save your code into a new file or files and give them the appropriate digit prefix to influence load order. By convention code in a '.cc' or '.wart' file stores its unit tests in the corresponding '.test.cc' or '.test' file respectively. To understand a strange name, find its tests. --- Keyword args You can change the order of arguments to a function -- or make their meaning more obvious -- by adding keywords. Keywords begin with a colon and are always optional. Wart will first scan the keywords in a call, then bind the remaining arguments in order. wart> (def foo(a b) (list a b)) wart> (foo 1 3) (1 3) wart> (foo :b 1 3) (3 1) wart> (foo 1 :a 3) (3 1) wart> (foo :b 1 :a 3) (3 1) Keywords that don't match function parameters are passed through as arguments: wart> (foo :c 1 2) (:c 1) ; the third arg gets dropped (Keyword symbols starting with a colon always evaluate to themselves.) This is useful primarily because you can pass keywords through layers of function calls: wart> (def A params (B @params)) ; all params get spliced into a call to B wart> (def B(c . d) (list c @d)) ; d gets remaining args as in scheme wart> (A 1 2 3) (1 2 3) wart> (A 1 2 :c 3) (3 1 2) If you want to refer to a param using a different keyword, use param aliases: def test(msg pred/should expr/valueof expected) .. Now test can refer to 'pred' and 'expr', but ':should' or ':valueof' may be more readable in calls to it. --- Optional parens Wart is indentation sensitive. Multi-word lines without leading parens are implicitly grouped with later indented lines: if (> n 0) * n (- n 1) => (if (> n 0) (* n (- n 1)) No indented lines after? They're grouped in isolation: a b c d => (a b) (c d) Lines with a single word are never wrapped in parens: def foo() x => (def foo() x) ; x is returned as a value, never called Lines with a leading paren are never wrapped in parens: def foo(x) (prn x) x => (def foo(x) (prn x) x) Putting these rules together, parens are not required around 'if' in: if (iso 1 (% x 2)) 'odd 'even ..but they are required in: (if ; parens required because line has a single word (iso 1 (% x 2)) ; parens required to avoid grouping with next line 'odd :else ; optional, sometimes more clear 'even) ..and, furthermore, this is wrong: if (iso 1 (% x 2) 'odd :else 'even => (if (iso 1 (% x 2)) 'odd) :else 'even ; wrong --- "But I hate significant whitespace!" I'm not trying to minimize parens typed; I'm trying to make lisp code more readable to non-lispers. Wart's codebase tastefully removes parens from just control-flow operators (def/mac/if/while/..), leaving them alone everywhere else. When in doubt, I insert parens. If you don't like this approach, just use parens everywhere: (def foo() 34) (def foo() 34) Indentation-sensitivity is disabled inside parens. This rule is useful if you want multiple expressions on a single line: (if test1 body1 ; parens required to avoid grouping test2 and body2 test2 body2 else-expr) It also implies that backquoted expressions must be fully parenthesized: mac when(cond . body) `(if ,cond (do ,@body)) ; parens before 'do' are required --- Simple syntax Wart uses a few simple syntax rules to reduce parens further: a.b ; => (a b) unless a and b are all digits; 2.4 is a number a. ; => (a) a!b ; => (a 'b) !a ; => (not a) f:g ; => (compose f g) f&g ; => (andf f g) ~f ; => (complement f) The prededence rules for these operators are intended to be as intuitive as possible, and it's easy to see what they expand to at the prompt: wart> 'a.b!c ((a b) 'c) --- Garbage collection Wart frees up unused memory using reference counting. Every Cell tracks the number of incoming pointers to it using a field called nrefs. Cycles must be explicitly broken to be collected. Wart must increment/decrement nrefs when saving a Cell inside another: in the car or cdr, or inside a Table. Never assign to car or cdr or a Table key by hand. Use an existing primitive: setCar, setCdr, set. They're thoroughly tested. Tests thoroughly audit nrefs using checkState, in order to detect errors as soon as possible. eval takes pains to increment the nrefs of *exactly* one Cell (the return value) across all paths. Nested calls to eval decrement nrefs of the result unless they return it. Structure other functions returning Cells the same way. To implement operations in C, use the COMPILE_FN macro. Each operation, like eval, is responsible for ensuring that the nrefs of exactly one Cell is incremented across all paths. If you hack on wart's cc files and forget to decrement nrefs you have a memory leak. If you forget to increment nrefs you have something worse: a prematurely garbage-collected cell that may now be used for something else entirely. Old pointers to it can no longer rely on what it contains; they may clobber or try to interpret arbitrary data as a string or a Table. All bets are off. Wart tries to loudly detect this insidious class of error as immediately as possible. Every time a Cell is freed it resets its pointers to NULL; when it's reused its pointers are initialized to nil. If wart ever complains that it ran across NULL, it means I or you forgot to decrement nrefs somewhere. All bets are off until it is fixed. --- Generated _lists To run its tests wart needs a list of tests to run. It constructs such a list on the fly during compilation. Several other places use the same trick. The set of files to compile is auto-generated, as is the list of compiled primitives wart knows about. You can add new CompiledFns, or entirely new code in files new or old, and they'll be automatically included the next time you run wart. --- Credits Wart was inspired by Arc, a lisp dialect by Paul Graham and Robert Morris: http://www.paulgraham.com/arc.html Discussions on the Arc Forum generated all the ideas here: http://arclanguage.org/forum Story arc (pun intended): "The wart atop the mountain": http://arclanguage.org/item?id=12814 Generic functions: http://arclanguage.org/item?id=11779, http://arclanguage.org/item?id=13790 Python-style keyword args: http://arclanguage.org/item?id=12657 Why wart has no modules: http://arclanguage.org/item?id=12777 Why wart has just one kind of equality: http://arclanguage.org/item?id=13690 In praise of late binding: http://arclanguage.org/item?id=15655 Libraries suck: http://www.arclanguage.org/item?id=13283 Feedback: firstname.lastname@example.org