Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

An experimental, small, readable lisp with thorough unit tests and extensible functions/macros.

tree: cd936ecf3e

Fetching latest commit…

Cannot retrieve the latest commit at this time

Readme
  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: wart@akkartik.com
Something went wrong with that request. Please try again.