DRAWL – LISP 1.5 written in PLASMA for the Apple II

Introduction

Why LISP 1.5? Didn’t that come out in the early 1960’s? Yes, as one of the earliest programming languages, LISP holds a certain place in history. Along with COBOL and FORTRAN, LISP hails from the dawn of computing and introduced many concepts still currently in use. Some unique features of the time included functional programming based on Lambda Calculus and recursion. Written as an interpreter, is was one of the first environments to use interactivity through a REPL interface: Read, Execute, Print, Loop.

As if exploring an ancient dialect of LISP from 1962 wasn’t enough, I wanted to do it using my own language, PLASMA, designed for the 6502 processor from 1975 and using it on an Apple ][ from 1977, all in 2024. I used the second edition of the LISP 1.5 manual to implement my own version of LISP 1.5, including some of the extensions to make LISP a more palatable language for the modern age (late 1960’s).

Some quick basics (no pun intended) of the language; everything is written as an S-expression. An S-expression is a simple notation that Uses parenthesis to denote a list of elements separated by spaces. Additionally, a dot notation can be used to pair elements together. Elements themselves can also be S-expressions. Since LISP is a programming language, an S-expression can represent a function to be executed by making the first element in the list the function and the remaining elements the arguments. A quick example:

(+ 1 4)

This would denote addition of two numbers, one and four, returning the value 5. Higher level functions can be defined with the LAMBDA notation:

(DEFINE (ADD1 (LAMBDA (I) (+ I 1))))

And that is where all the parentheses comes in to play. I won’t spend any more time on the actual language, there are links at the bottom for that.

A Brief History of LISP

LISP was developed by John McCarthy at the M.I.T. Artificial Intelligence Lab starting around 1957. The first widely available version was LISP 1.5 around 1962. McCarthy based LISP on the idea of Lambda Calculus proposed by Alonso Church in the early 1930’s. A simple concept of calculation involving recursion. LISP 1.5 was an interactive interpreter that could also compile into machine code while running. Pretty heady stuff in the early days of computing.

Unsurprisingly, only being able to program using recursive S-expressions was a bit limiting and not always easy. Sometimes the direct approach works better. So extensions were developed for LISP to make it a bit more like its contemporary languages such as FORTRAN and ALGOL. With the extensions, LISP becomes a little less weird and more approachable, but the parenthesis remain.

The listing in the LISP 1.5 manual didn’t help in that they didn’t use the idea of formatting and indenting to clarify levels and blocks of code. I will use formatting in my examples to improve the readability of the code.

PLASMA, the Apple II, and LISP 1.5

When deciding to do my deep-dive into understanding LISP at a fundamental level, I used the old adage: “To fully understand a language, you need to write an interpreter/compiler for it”. There aren’t any modern versions of LISP 1.5 available – they are all based on Common Lisp, Scheme, Closure, etc. and are much larger languages than I wanted to implement on my own. I also try and push my own language, PLASMA, to develop sophisticated applications, utilities, and languages. My last project involved implementing FORTH in PLASMA. It wasn’t a huge leap as I used many FORTH concepts in the development of the PLASMA byte code VM. LISP has been a much more challenging journey in that it was new territory for me.

DRAWL

As a play on LISP, DRAWL is a slow version of LISP 1.5 for the Apple II and ///. Turns out, it isn’t as slow as I expected. It’s rather interactive and capable of actual algorithm development. Some of the notable features of DRAWL: tail recursion, 32 bit integers, 80 bit floating point, arrays, printer output, lo-res graphics and garbage collection. There are many more features detailed on the GitHub site. DRAWL uses a great many features of the PLASMA environment, and as such, uses a lot of memory for itself. On a 64K Apple II, it only leaves about 8K of free memory for LISP code and objects. However, if transcendental functions are not needed, they can be skipped at startup to free up to about 10K of memory. On a 128K machine, there is about 24K of free memory, even with the transcendental functions loaded. PLASMA make efficient use of memory in a 128K machine, so the added features don’t impact the memory footprint as much. DRAWL incorporates some of the later extensions to LISP 1.5 to support more ALGOL like code. The examples I’ll go over compare the two approaches.

DRAWL lacks general I/O operations, but does allow for LISP code to be loaded from a file. Routines for reading the keyboard and checking for key presses gives decent interactivity to programs. For output, echoing to a printer provides hard copy. If running in an emulated environment, it often captures printer output to a local file and/or the clipboard making incorporating calculations into outside operating system easy.

LISP 1.5, and thus DRAWL, is really focused on mathematical expressions. DRAWL uses 32 bit integers for extended values over most Apple II programming environments. Bit operations and hexadecimal notation make for a handy low-level calculator. Floating point support comes from the Standard Apple Numeric Environment library. It calculates using an 80 bit extended floating point format, rivaling most modern implementations. DRAWL can operate as a very sophisticated programmable calculator.

LOOP.LISP example

This example uses three different techniques to loop over a range of integers from 1 to 100 and print them out:

(DEFINE
 (TAILLOOP (LAMBDA (I M)
   (COND ((AND (< I M) (PRIN I)),
           (TAILLOOP (+ 1 I) M))
         (T,(- I 1)))
 ))
 (PROGLOOP (LAMBDA (I M) (PROG ()
   A (PRIN I)
     (SETQ I (+ I 1))
     (IF (< I M)
       (GO A))
     (RETURN (- I 1))
 )))
 (FORLOOP (LAMBDA (I M)
   (FOR I I 1 (< I M) (PRIN I))
 ))
)

'TAIL
(TAILLOOP 1 100)
'PROG
(PROGLOOP 1 100)
'FOR
(FORLOOP 1 100)

The first function, TAILLOOP(), recursively calls itself with an incrementing value of I. The terminal count, M, is passed along each time so COND() can choose when to return a value instead of recursing. Note I am using AND() to call two functions in the test clause. PRINI() will pass the value printed through as it’s value. Anything other than NIL will be considered TRUE. So, if the terminal count hasn’t been reached AND PRINI() returns a value other than NIL (always in this case), the second clause will run and TAILLOOP() will be tail called with the incremented value of I. The sharp eye will notice there are commas separating the elements of the COND() test and execute clauses. The manual mentions the comma is a hold-over from the earlier version of LISP and should be treated as a space. I use it here for the visual distinction between the clauses, although unnecessary.

The second function, PROGLOOP(), employs the ALGOL-like construct PROG() to execute functions in order. Two new operations, GO() and RETURN() control the flow of the routine. GO() is a GOTO LABEL function. RETURN() simply returns from the function with a value. SETQ() will update the value of a variable with the second argument.

Lastly, the FORLOOP() uses the FOR(…) extension and runs the first argument (must be a variable) from an initial value (itself in this case), incrementing it with the third argument and testing with the fourth argument. If the test returns true, the rest of the arguments are called as functions in order, the value of the loop being that of the final function in the list.

As for performance, the TAILLOOP() and PROGLOOP() are roughly equivalent. The FORLOOP() runs significantly faster due to some index variable updates happening under the covers.

LORES.LISP

This example shows off a number of LISP and DRAWL features:

(DEFINE
 (PLOTFUNC (LAMBDA (FN) (PROG
   (I X Y)
   (FOR I 0 1 (< I 40)
     (SETQ X (/ (- I 19.5) 20.0))
     (SETQ Y (FN X))
     (AND (> Y -1.0) (< Y 1.0)
       (PLOT I (- 19.5 (* Y 20.0))))
   )
   (RETURN 0)
 )))
 (PLOTSIN (LAMBDA ()
   (PLOTFUNC '(LAMBDA (S) (SIN (* S *PI*))))
 ))
 (PLOTCOS (LAMBDA ()
   (PLOTFUNC '(LAMBDA (S) (COS (* S *PI*))))
 ))
)
(GR T)
(COLOR 2)
(PLOTSIN)
(COLOR 9)
(PLOTCOS)

Here, PLOTFUNC() takes a parameter being a function to call. PLOTFUNC() steps through the 40 horizontal pixels (0 to 39), scales it into an X value going from -1.0 to 1.0, then calls the passed in function to calculate a Y value. The Y value is scaled back into the vertical pixel range of 0 to 39 and plots it. Here are a few things to note: the Y value is tested for being between -1.0 and 1.0 and PLOT(). AND() will short-circuit should it come to a FALSE clause and skip the rest of the arguments. If Y is out of range, PLOT() won’t be called. PLOTSIN() and PLOTCOS() both pass in a function by quoting it with the ‘ character (shorthand for QUOTE()). The LISP aficionado will say this should be written as:

 (PLOTSIN (LAMBDA ()
   (PLOTFUNC (FUNCTION (LAMBDA (S)
               (SIN (* S *PI*)))))
 ))
 (PLOTCOS (LAMBDA ()
   (PLOTFUNC (FUNCTION (LAMBDA (S)
               (COS (* S *PI*)))))
 ))

and they are correct. Mostly. The ‘ character, or QUOTE(), is fine in this case as the functions don’t access free variables – variables defined outside the function. In this case, QUOTE() is a smidge faster than FUNCTION().

Output:

ROD.LISP

We’re pulling out many of the extensions made to DRAWL to show that LISP 1.5 can look very much like a standard programming language. Comments start with the ‘;’ character and run to the end of the line:

;
; Rod's Colors
;
(define (rod (lambda () (prog
       (i j k w fmi fmk clr) ; local vars

       (gr t)
       (gotoxy 11 1)
       (print "Press any key to exit.")
  loop (for w 3 1 (< w 51)
       (for i 1 1 (< i 20)
       (for j 0 1 (< j 20)
          (= k (+ i j))
          (= clr (+ (/ (* j 3) (+ i 3)) 
                    (/ (* i w) 12)))
          (= fmi (- 40 i))
          (= fmk (- 40 k))
          (color clr)
          (plot i k)
          (plot k i)
          (plot fmi fmk)
          (plot fmk fmi)
          (plot k fmi)
          (plot fmi k)
          (plot i fmk)
          (plot fmk i)
          (if (keypressed?)
            (return (and (readkey) (gr f))))
       )))
       (go loop)
))))

(rod)
"That's all, folks!"

This was a direct port of Rod’s Colors that I’ve written in many different languages from Java to PLASMA to FORTH. I found the original somewhere in BASIC. There are a few functions that implement lo-res graphics like in LORES.LISP, but also see some direct keyboard functions. There is the alias for SETQ(), the ‘=’ character making for a shorter and easier to read function.

Strings

DRAWL adds extensions for string handling that didn’t exist in LISP 1.5. Creating, extracting and combing strings as well as converting between character codes and characters provides flexibility when dealing with strings and characters. An interactive session with string functions:

?(lens "Now is the time for all good men")
 32

?(subs "Now is the time for all good men" 11 4)
time

?(cats "super" "duper")
superduper

?(chars 75 76 77 78 79)
KLMNO

?(string 10.3e-2)
0.103

?(number "10.3e-2")
 0.103
Macros

DRAWL provides MACRO functions to enable meta-programming. Basically extending the language. Check out the macros in DEFUN.LISP:

;
; USE MACRO TO SIMPLIFY FUNCTION DEFINITION
;
(DEFINE (CADR  (LAMBDA (L) (CAR (CDR L))))
        (CDDR  (LAMBDA (L) (CDR (CDR L))))
        (CADDR (LAMBDA (L) (CAR (CDR (CDR L)))))
        (CDDDR (LAMBDA (L) (CDR (CDR (CDR L)))))
        (DEFUN (MACRO (L)
          (EVAL (CONS 'DEFINE
               (LIST (CONS (CAR L)
                 (LIST (CONS 'LAMBDA
                             (CDR L)))))))
        ))
        (DEFPRO (MACRO (L)
          (EVAL (CONS 'DEFINE
               (LIST (CONS (CAR L)
                 (LIST (CONS 'LAMBDA 
                   (LIST (CADR L)
                         (CONS 'PROG (CDDR L))
               )))))
          ))
        ))
        (DEFVAL (MACRO (L)
          (EVAL (CONS 'CSETQ L))
        ))
)

Although it looks a bit daunting, it provides simplified function and procedure definitions. Specifically for writing procedures with the PROG(…) extension using the above DEFPRO(…) macro:

(DEFPRO TEST (ARG1, ARG2)     ; ARGUMENTS
             (LOCAL1, LOCAL2) ; LOCAL VARS
                              ;
             (= LOCAL1 (+ ARG1 ARG2))
             (= LOCAL2 (* ARG1 ARG2))
             (PRINT LOCAL1 LOCAL2)
             (RETURN (- LOCAL2 LOCAL1))
)

Conclusion

I hope this quick introduction to DRAWL shows off its capabilities without drowning you in obscure programming paradigms. The parenthesis are a bit much to take in, but with careful formatting they shouldn’t overwhelm the code. It would be nice if there were a LISP aware editor for the Apple II, but with modern emulation it makes it easier to edit in a more powerful environment and run on the simulated machine.

Links

Here are links to follow up if you’re interested. The LISP 1.5 manual is particularly interesting. Much different than texts today and gets into the interpreter implementation by page 13!

LISP 1.5 Manual: https://archive.org/details/bitsavers_mitrlelisprammersManual2ed1985_9279667

LISP 1.5 Primer: https://www.softwarepreservation.org/projects/LISP/book/Weismann_LISP1.5_Primer_1967.pdf

Personal-LISP Manual (Apple II version close to LISP 1.5): https://archive.org/details/gLISP/gnosisLISPManual

Apple Numerics Manual (SANE): https://vintageapple.org/inside_o/pdf/Apple_Numerics_Manual_Second_Edition_1988.pdf

Part 1 of DRAWL in action: https://youtu.be/wBMivg6xfSg

Part 2 of DRAWL in action: https://youtu.be/MdKZIrfPN7s

GitHub DRAWL documentation: https://github.com/dschmenk/PLASMA/blob/master/doc/DRAWL.md

Preconfigured PLASMA ProDOS boot floppy for DRAWL: https://github.com/dschmenk/PLASMA/blob/master/images/apple/DRAWL.po