http://t3x.org/mcl/mcl.t.html   (light|dark)

MICRO COMMON LISP

T3X SOURCE CODE   |   LISP SOURCE CODE

This is the low-level part of the source code of the MICRO COMMON LISP interpreter, written in T3X/0. There is also a translation of this program to C (C89, ANSI C), but the T3X version is where the development takes place.

!! MICRO COMMON LISP, A Microscopic Common LISP Subset
!! Nils M Holm, 2019, 2020, 2024, 2025
!! In the public domain or distributed under the 0BSD license

! This program will use basic memory operations and I/O
! functions, as well as the console interface.

use t3x: t;
use console: con;

! ******************************
! ***** GLOBAL DEFINITIONS *****
! ******************************

! We will assume that the console has 80x24 characters,
! like a VT100 terminal.

! Note: COLUMNS cannot be larger than BUFLEN, below.

const	COLUMNS = 80,
	LINES	= 24;

! There are NNODES Nodes in the node pool. A Cons cell
! allocates one node, a symbol uses 4+N nodes, where N
! is one per two characters in the symbol name, i.e.
! one for one- and two-character names, two for three-
! and four-character names, etc.

const	NNODES	= 7168;

! A Symbol can be up to 64 characters long.

const	SYMLEN	= 64;

! I/O buffers are 128 characters wide. Note that buffers
! are mostly used for keyboard input, so 128 is plenty.
! There is not much disk I/O going on in this program.

! ***** Increasing this value will add to memory footprint
! ***** significantly!

const	BUFLEN	= 128;

! EXDEPTH is how often the macro expander can recurse.
! That is, this is roughly the number of clauses that
! can be in a single COND or bindings that can be in
! a single LET*.

const	EXDEPTH	= 128;

! When printing a list that is nested more than PRDEPTH
! times, printing will be aborted (assuming that the
! printed structure is cyclic).

const	PRDEPTH	= 64;

! NLOAD is the number of times LOAD can be nested, where
! one would mean that a LOADed program cannot LOAD another.

! ***** Increasing this value will add to memory footprint
! ***** significantly!

const	NLOAD	= 3;

! Special Nodes get values outside of the node pool.
! Valid node addresses are zero through NNODES-1.
! A special node is a node that represents some object
! that is neither a symbol nor a cons.

const	SPCL	= NNODES;

! Special nodes: NIL, End of Text, the dot in dotted
! pairs, the right parenthesis, and the "undefined"
! value for indicating that a symbol is not bound.

! Yes, NIL is a special object in this implementation.
! This detail will be invisible to the user, though.

const	NIL	= NNODES+1;
const	EOT	= NNODES+2;
const	DOT	= NNODES+3;
const	RPAREN	= NNODES+4;
const	UNDEF	= NNODES+5;

! ****************************
! ***** STRING FUNCTIONS *****
! ****************************

! We could import the STRING module, but we only need
! a few functions, so let's define them here.

! The length of a string, limited to 32766 characters.

strlen(s) return t.memscan(s, 0, 32767);

! Are two strings equal?

strequ(a, b) return t.memcomp(a, b, strlen(a)+1) = 0;

! Copy string B to A.

strcopy(a, b) t.memcopy(a, b, strlen(b)+1);

! Convert an unsigned integer to an ASCII string and
! return a pointer to the string. An internal buffer
! will be used for the string, so subsequent calls to
! NTOA will overwrite the buffer.

var Ntoab::20;

ntoa(n) do var q, p;
	p := @Ntoab::19;
	q := p;
	p::0 := 0;
	while (n \/ p = q) do
		p := p-1;
		p::0 := n mod 10 + '0';
		n := n ./ 10;
	end
	return p;
end

! *****************************
! ***** CONSOLE INTERFACE *****
! *****************************

! The interpreter will use the CONSOLE interface of T3X/0,
! so it can provide some basic command line editing.

! XPOS and YPOS are the X and Y positions of the cursor on
! the screen.

var	Xpos, Ypos;

! Clear the screen.

clear() do
	con.clrscr();
	Xpos := 0;
	Ypos := 0;
end

! When a message is posted to the status line of the
! interpreter, store it in MSG, so it can be redrawn
! after scrolling the screen.

var	Msg;

! Batch mode disables the CONSOLE interface. When the
! interpreter runs in Batch mode (BATCH=%1), command line
! editing is disabled and input is read directly from
! SYSIN. No messages will be posted to the status line.

var	Batch;

! Post a message to the status line.

message(s) do
	if (Batch) return;
	con.move(0, LINES-1);
	con.standout();
	con.clreol();
	con.writes(s);
	con.normal();
	con.move(Xpos, Ypos);
	Msg := s;
end

! Scroll the screen up by one line
! without scrolling the status line.

scroll() do
	con.move(0, LINES-1);
	con.clreol();
	con.scroll();
	message(Msg);
end

! Print the string S on the console screen. In Batch mode,
! just write the string to SYSOUT.

! When writing to the console, interpret CR and LF characters
! and scroll the screen when necessary.

pr(s) do var i;
	ie (Batch) do
		t.write(T3X.SYSOUT, s, strlen(s));
	end
	else do
		i := 0;
		while (s::i) do
			ie (s::i = '\n') do
				Ypos := Ypos+ 1;
				con.move(Xpos, Ypos);
			end
			else ie (s::i = '\r') do
				Xpos := 0;
				con.move(Xpos, Ypos);
			end
			else do
				con.wrch(s::i);
				Xpos := Xpos + 1;
			end
			if (Xpos >= COLUMNS) do
				Xpos := 0;
				Ypos := Ypos + 1;
				con.move(Xpos, Ypos);
			end
			if (Ypos >= LINES-1) do
				scroll();
				Ypos := LINES - 2;
				con.move(Xpos, Ypos);
			end
			i := i+1;
		end
	end
end

! Write a "newline" sequence to the console (or to SYSOUT
! in Batch mode).

nl() do var b::3;
	ie (Batch)
		pr(t.newline(b));
	else
		pr("\r\n");
end

! These are the History buffers. Three input lines will be
! kept in memory: the one currently being edited, and the
! two ones recently submitted.

var	Auxb1::BUFLEN, Auxb2::BUFLEN;

! Rotate the contents of the history buffers through the
! current editing buffer B, so that the content of AUXB1
! will end up in B, AUXB2 in AUXB1, and B in AUXB2:
!
! +---+      +-------+      +-------+
! | B | <--- | AUXB1 | <--- | AUXB2 | <---+
! +---+      +-------+      +-------+     |
!   |                                     |
!   +-------------------------------------+

rotatebufs(b) do var i, c;
	for (i=0, BUFLEN) do
		c := Auxb1::i;
		Auxb1::i := Auxb2::i;
		Auxb2::i := b::i;
		b::i := c;
	end
end

! The error handler will be defined later.

decl error(2);

! READCON reads a line from the console into the buffer B
! with command line editing. It returns the number of
! characters in B.

! The following keys are interpreted (^X = Control+x):
! ^A         Move to beginning of line
! ^B         Move left/back
! ^C         Abort input, signal an error
! ^D         Delete character; on empty line: send EOT
! ^E         Move to end of line
! ^F         Move right/forward
! Backspace  Delete character on the left
! ^L         Clear screen
! Enter      Submit input
! ^P         Go through history
! ^U         Delete line, keep editing

readcon(b) do var p0, p, r, m, k, n;
	m := COLUMNS - 4;
	p0 := Xpos;
	p := 0;
	r := 1;
	n := 0;
	b::0 := 0;
	while (%1) do
		if (r) do
			con.move(p0, Ypos);
			con.writes(b);
			con.wrch('\s');
			r := 0;
		end
		con.move(p+p0, Ypos);
		k := con.getkey();
		ie (k = 2) do		! ^B
			if (p > 0) p := p-1;
		end
		else ie (k = 6) do	! ^F
			if (p < n) p := p+1;
		end
		else ie (k = 1) do	! ^A
			p := 0;
		end
		else ie (k = 5) do	! ^E
			p := n;
		end
		else ie (k = 12) do	! ^L
			clear();
			if (p0) pr("* ");
			r := 1;
		end
		else ie (k = 16) do	! ^P
			rotatebufs(b);
			n := strlen(b);
			p := n;
			r := 1;
			con.move(Xpos, Ypos);
			con.clreol();
		end
		else ie (k = '\r' \/ k = '\n') do
			if (n > 0) do
				if (Auxb1::0 \= 0)
					strcopy(Auxb2, Auxb1);
				strcopy(Auxb1, b);
			end
			b::n := k;
			n := n+1;
			b::n := 0;
			nl();
			return n;
		end
		else ie (k = 3) do	! ^C
			nl();
			b::0 := 0;
			error("abort", UNDEF);
			return 0;
		end
		else ie (k = 21) do	! ^U
			b::0 := 0;
			n := 0;
			p := 0;
			r := 1;
			con.move(p0, Ypos);
			con.clreol();
		end
		else ie ((k = '\b' \/ k = 127) /\ p > 0 \/
			 k = 4 /\ p < n)
		do
			n := n-1;
			if (k \= 4) p := p-1;
			t.memcopy(@b::p, @b::(p+1), n-p);
			b::n := 0;
			r := 1;
		end
		else ie (k = 4 /\ n = 0) do	! ^D
			b::n := 4;
			b::(n+1) := 0;
			pr("^D");
			nl();
			return n;
		end
		else if ('\s' <= k /\ k <= '~') do
			if (n >= m) loop;
			t.memcopy(@b::(p+1), @b::p, n-p);
			b::p := k;
			p := p+1;
			n := n+1;
			b::n := 0;
			r := 1;
		end
	end
end

! ***************************
! ***** INPUT BUFFERING *****
! ***************************

! Current input file.

var	Input;

! Input buffer, pointer to next character, number
! of characters in the buffer.

var	Inbuf, Inp, Ink;

! Default input buffer, used for console input

var	Buffer::BUFLEN;

! Single-character reject buffer for putting characters
! back for later reading. When Rejected=EOT, there is no
! character in the reject buffer.

var	Rejected;

! Line counter for reporting errors in files.

var	Line;

! Flush any pending input and restore the default
! input channel. This will be done when initializing
! the system, and also in case of an error so the
! interpreter does not keep reading input after
! aborting.

flushinp() do
	Input := T3X.SYSIN;
	Inbuf := Buffer;
	Inp := 0;
	Ink := 0;
	Rejected := EOT;
end

! RDCH reads a single character through the buffered
! interface and returns it. It returns EOT when reading a
! Control-D (4) or Control-Z (26) character or when no
! input can be read from the current input file or device.

! Note that Control-Z is for DOS and CP/M. The console
! interface does not pass through Control-Z.

rdch() do var c;
	if (Rejected \= EOT) do
		c := Rejected;
		Rejected := EOT;
		return c;
	end
	if (Inp >= Ink) do
		ie (Input = T3X.SYSIN /\ \Batch)
			Ink := readcon(Inbuf);
		else
			Ink := t.read(Input, Inbuf, BUFLEN);
		if (Ink < 1) return EOT;
		Inp := 0;
	end
	c := Inbuf::Inp;
	Inp := Inp+1;
	if (c = 4 \/ c = 26) return EOT;
	if (Line /\ c = '\n') Line := Line + 1;
	return c;
end

! Read a single character and convert letters to
! upper case. CI = case insensitive.

rdchci() do var c;
	c := rdch();
	if (c >= 'a' /\ c <= 'z') return c-32;
	return c;
end

! **************************
! ***** ERROR HANDLING *****
! **************************

! When ERRFLAG is set, no more input will be read,
! evaluation will stop, and no more errors will be
! reported.

var	Errflag;

! The S-expression printer and the shutdown routine
! will be defined later.

decl print(1), fini(1);

! ERROR is called whenever an error is detected while
! reading or evaluating an expression. It prints the
! error message M and the optional argument N, flushes
! pending input and sets the error flag. In Batch mode
! it also shuts down the interpreter.

error(m, n) do
	if (Errflag) return;
	pr("? ");
	if (Line) do
		pr(ntoa(Line));
		pr(" ");
	end
	pr(m);
	if (n \= UNDEF) do
		pr(": ");
		print(n);
	end
	nl();
	flushinp();
	Errflag := %1;
	if (Batch) fini(1);
	return NIL;
end

! FATAL is called in case of unrecoverable errors.
! It always shuts down the interpreter.

fatal(m) do
	error(m, UNDEF);
	pr("? aborting");
	nl();
	fini(1);
end

! ****************************************************
! ***** MEMORY ALLOCATION AND GARBAGE COLLECTION *****
! ****************************************************

! The node pool pool consists of three arrays, one
! containg the CAR fields of Conses, one containing
! their CDR fields, and one containing the tag bits
! of the node.

! A node with no tag bits set is a Cons cell. Symbols
! are composed of multiple nodes, of which some have
! the ATOMB bit set. This will be explained in greater
! detail further below.

! A Cons node has an address in both the CAR and CDR
! field. An atomic node has a value in the CAR field
! and an address in the CDR field. All addresses are
! offsets in the CAR, CDR, and TAG arrays.

var	Car[NNODES],
	Cdr[NNODES];

var	Tag::NNODES;

const	ATOMB	= 0x01;

! The MARKB and SWAPB tag bits are used during garbage
! collection. See below.

const	MARKB	= 0x02;
const	SWAPB	= 0x04;

! The Freelist is a linked list of free nodes. The CAR
! field of each free node is unimportant (the node is
! inactive anyway), and the CDR field has the address
! of the next free node.

! As long as the freelist is not empty, allocating a
! node is a fast and simple as removing the head of the
! freelist and returning its address.

var	Freelist;

!! Deutsch/Schorr/Waite graph marker

! When the Freelist is empty, the garbage collector
! marks all nodes that are currently used by the LISP
! system and then adds all unmarked nodes to the
! Freelist. A node is used, if it is either a symbol
! used by the system or part of a cons tree used by
! the system.

! The MARK procedure marks all nodes that are part of
! the cons tree whose root is N.

! It uses the constant-space Deutsch/Schorr/Waite graph
! marking algorithm, also known as the Pointer Reversal
! algorithm. Its fundamental idea is to store the address
! of the parent node in the current node instead of a
! stack. Hence it needs no extra space.

! It works like this:
! A = CAR field      D = CDR field
! N = current node   P = parent of N   G = grandparent of N
! --- = untraversed node (MARKB=0)     S = SWAPB bit
! === = traversed node   (MARKB=1)
!
!     N R  P=G        P R             P R               R      
!      \|              \|              \|               |      
!     +===+           +===+           +===+           +===+    
!     |A|D| S=0       |G|D| S=1       |A|G| S=0       |A|D| S=0
!     +===+       N   +===+           +===+   N       +===+    
!    /     \       \ /     \         /     \ /       /     \   
! +---+   +---+   +===+   +---+   +===+   +===+   +===+   +===+
! |A|D|   |A|D|   |A|D|   |A|D|   |A|D|   |A|D|   |A|D|   |A|D|
! +---+   +---+   +===+   +---+   +===+   +===+   +===+   +===+
!
!      (1)             (2)             (3)             (4)

! (1) N points to an untraversed tree with root R. Every node
!     is marked (MARKB=1) as soon as it is visited.
! (2) When moving the pointer N from the node R to the CAR child
!     of R, the parent pointer P moves the R. Of course now the
!     parent of the parent (G) has to be stored somewhere.
!     Because N=CAR[P], we can use CAR[P] to store G. We also set
!     the SWAPB bit (S=1) of P.
! (3) When returning to P after traversing CAR[P], the SWAPB bit
!     of P is one, so CDR[P] also needs to be traversed. The
!     same mechanism is used, only G is stored in CDR[P] now, and
!     the original CAR[P] is restored from N. The SWAPB bit of P
!     is cleared.
! (4) When returning to P after traversing CDR[P], the SWAPB bit
!     of P is zero. CDR[P] is restored from N, and traversal
!     returns to G by setting P=G and then N=P.

! When an already-marked node or a special node is found,
! traversal returns to the parent immediately. When an ATOM is
! found (ATOMB=1), its CAR field is ignored and traversal
! immediately proceeds with its CDR field. When returning
! from a node with no parent (P=NIL), traversal ends and the
! entire tree N has been marked.

mark(n) do var p, x;
	p := NIL;
	while (%1) do
		ie (n >= SPCL \/ Tag::n & MARKB) do
			if (p = NIL) leave;
			ie (Tag::p & SWAPB) do	! (3)
				x := Cdr[p];
				Cdr[p] := Car[p];
				Car[p] := n;
				Tag::p := Tag::p & ~SWAPB;
				n := x;
			end
			else do			! (4)
				x := p;
				p := Cdr[x];
				Cdr[x] := n;
				n := x;
			end
		end
		else ie (Tag::n & ATOMB) do
			x := Cdr[n];
			Cdr[n] := p;
			p := n;
			n := x;
			Tag::p := Tag::p | MARKB;
		end
		else do				! (2)
			x := Car[n];
			Car[n] := p;
			Tag::n := Tag::n | MARKB | SWAPB;
			p := n;
			n := x;
		end
	end
end

! Objects pointed to by the following variables are currently
! used by the LISP system. The variables are called "GC Roots",
! because they are the roots of trees in the node pool. Marking
! the GC roots with MARK will mark all currently used nodes.

! SYMBOLS is the list of all symbols known by the system.
! A symbol contained in this list is an Interned Symbol.

var	Symbols;

! ACC is the Accumulator: a register that contains a (partially
! evaluated) expression during the process of evaluation.

var	Acc;

! ENV is the current lexical environment. Its structure will be
! outlined later.

var	Env;

! STACK is a stack (list) that is used to store all kinds of
! temporary values during evaluation, like suspended environments
! or the addresses of nodes that need temporary protection from
! the GC.

var	Stack;

! MSTACK (Mode Stack) is a stack of atoms that indicate the current
! state (mode) of the evaluator.

var	Mstack;

! TMPCAR and TMPCDR protect the CAR and CDR arguments while
! allocating a cons cell.

var	Tmpcar, Tmpcdr;

! The ZNODE is a pair of the form (NIL . NIL) that is
! returned by the node allocator when running out of
! nodes. Some cons cell has to be returned, because
! code may attempt to modify the returned CONS, so just
! returning NIL is not an option.

var	Znode;

! When the VERBOSE_GC flag is set, the number of free
! nodes will be reported after each garbage collection.
! This is not a GC root.

var	Verbose_GC;

! GC is a Mark & Sweep garbage collector.
! In the Mark phase, it marks all GC roots.
! In the Sweep phase, it adds all unmarked
! nodes to the Feelist and unmarks all marked
! nodes.

gc() do var i, k;
	if (Verbose_GC) pr("GC: ");
	mark(Znode);
	mark(Acc);
	mark(Env);
	mark(Symbols);
	mark(Stack);
	mark(Mstack);
	mark(Tmpcar);
	mark(Tmpcdr);
	k := 0;
	Freelist := NIL;
	for (i=0, NNODES) do
		ie (Tag::i & MARKB) do
			Tag::i := Tag::i & ~MARKB;
		end
		else do
			Cdr[i] := Freelist;
			Freelist := i;
			k := k+1;
		end
	end
	if (Verbose_GC) do
		pr(ntoa(k));
		pr(" NODES");
		nl();
	end
	return k;
end

! CONS3 is the principal node allocator of the LISP system.
! A is the CAR value of the new node, D is its new CDR value,
! and if TA is non-zero, the new node is an atomic node.
! CONS3 always returns a node.
! When no free nodes are available, it reports an error and
! the computation in progress is aborted.

cons3(a, d, ta) do var n;
	if (Freelist = NIL) do
		Tmpcdr := d;
		if (\ta) Tmpcar := a;
		gc();
		Tmpcar := NIL;
		Tmpcdr := NIL;
		if (Freelist = NIL) do
			error("out of nodes", UNDEF);
			return Znode;
		end
	end
	n := Freelist;
	Freelist := Cdr[Freelist];
	Car[n] := a;
	Cdr[n] := d;
	Tag::n := ta;
	return n;
end

! CONS allocates and returns a fresh CONS cell.

cons(a, d) return cons3(a, d, 0);

! SAVE saves the node N on the stack.

save(n) Stack := cons(n, Stack);

! UNSAVE removes K nodes from the stack and returns the
! one removed last. E.g. given the stack (N1 N2 N3 N4)
! calling UNSAVE(2) will return N2 and leave N3 and N4 on
! the stack.

unsave(k) do var n;
	while (k) do
		if (Stack = NIL) fatal("stack empty");
		n := Car[Stack];
		Stack := Cdr[Stack];
		k := k-1;
	end
	return n;
end

! *********************
! ***** ACCESSORS *****
! *********************

! These accessors will only be used in this file.
! The actual LISP functions will be defined in the
! LISP part of the system.

caar(x) return Car[Car[x]];
cadr(x) return Car[Cdr[x]];
cdar(x) return Cdr[Car[x]];
cddr(x) return Cdr[Cdr[x]];

caadr(x) return Car[Car[Cdr[x]]];
cadar(x) return Car[Cdr[Car[x]]];
caddr(x) return Car[Cdr[Cdr[x]]];
cdadr(x) return Cdr[Car[Cdr[x]]];
cddar(x) return Cdr[Cdr[Car[x]]];
cdddr(x) return Cdr[Cdr[Cdr[x]]];

caddar(x) return Car[Cdr[Cdr[Car[x]]]];
cdddar(x) return Cdr[Cdr[Cdr[Car[x]]]];

! ***********************************
! ***** SYMBOL TABLE MANAGEMENT *****
! ***********************************

! MKSYM creates an Uninterned Symbol from the string S.
! An uninterned symbols is a symbol that is NOT contained
! in the list of symbols known to the system. Comparing
! uninterned symbols with EQ will always yield NIL.

! The internal structure of an uninterned symbol is as
! follows:
!
!     +---+-------+
!     | O | UNDEF |
!     +-|-+-------+
!       |
!       V
!   +---+---+      +---+---+
!  A|C12| O-----> A|C34| O------> ...
!   +---+---+      +---+---+
!
!  A   = ATOMB bit set
!  C12 = first and second character of the symbol name
!  C34 = third and fourth character of the symbol name (if any)
!
! There is one atomic node for each two characters of the
! symbol name. When the name has an odd number of characters,
! the last character will be NUL.

mksym(s) do var i, n;
        i := 0;
        if (s::i = 0) return NIL;
	n := cons3(NIL, NIL, ATOMB);
        save(n);
        while (%1) do
                Car[n] := s::i << 8 | s::(i+1);
                if (s::(i+1) = 0 \/ s::(i+2) = 0) leave;
                Cdr[n] := cons3(NIL, NIL, ATOMB);
                n := Cdr[n];
                i := i + 2;
        end
        n := unsave(1);
        return cons(n, UNDEF);
end

! SYMSTR returns a string containing the name of the
! (interned of uninterned) symbol N. It writes the name
! to an internal buffer. Subsequent calls to SYMSTR will
! overwrite the buffer.
! SYMSTR returns a pointer to the name in the buffer.

var Symb::SYMLEN+2;

symstr(n) do var i;
        i := 0;
        n := Car[n];
        while (n \= NIL) do
                Symb::i := Car[n] >> 8;
                Symb::(i+1) := Car[n] & 0xff;
                i := i + 2;
                n := Cdr[n];
        end
        Symb::i := 0;
        return Symb;
end

! Find the symbol named S in the list of interned symbols.
! If the symbol is in the list, return it, and otherwise
! return NIL.

findsym(s) do var p;
	p := Symbols;
	while (p \= NIL) do
		if (strequ(s, symstr(Car[p])))
			return Car[p];
		p := Cdr[p];
	end
	return NIL;
end

! Create a pair of boxes for Value and Function bindings.
! Initialize with V=value and F=function.

mkboxes(v, f) return cons(v, cons(f, NIL));

! ADDSYM adds a new symbol to the list of interned symbols.
! When a symbol named S already exists, it returns it.
! Otherwise an uninterned symbol will be created and added
! to the SYMBOLS list. ADDSYM always returns a symbol named S.

! An interned symbol has a Value Box and a Function Box for
! binding it in the function and value namespaces. The internal
! structure of an interned symbol is similar to the structure
! of an uninterned symbol, except for the value boxes:
!
!                     Value Box     Function Box
!     +---+---+       +---+---+      +---+---+
!     | O | O-------> | O | O------> | O |NIL|
!     +-|-+---+       +-|-+---+      +-|-+---+
!       |               |              |
!       |               V              V
!       V             Value          Value
!   +---+---+
!  A|C12| O-----> ...
!   +---+---+

! Normally all value boxes of the new symbol will contain
! the special value UNDEF, which means that the symbol has no
! bindings. When the parameter SELF of ADDSYM is set to a
! non-zero value, though, the new symbol will refer to itself
! through the function box. This is how built-in functions
! are implemented in MICRO COMMON LISP. E.g.:
!
! (FUNCTION CONS)  ==>  CONS
!
! EVAL identifies built-in function by symbol identity.

addsym(s, self) do var n;
	n := findsym(s);
	if (n \= NIL) return n;
	n := mksym(s);
	Symbols := cons(n, Symbols);
	Cdr[n] := mkboxes(UNDEF, self-> n: UNDEF);
	return n;
end

! LOOKUP looks up the symbol X in the current Lexical
! Environment. If FBOX is zero, it looks up the value binding
! of the symbol, if FBOX=1 it looks up the function binding.
! When X is not in the current lexical environment, it looks
! up its global bindings.

! LOOKUP returns the requested binding box of the symbol.

! Note that the same box of a symbol can have multiple
! bindings in different environments, but every box of a
! symbol has only one global binding, which is outlined in
! the description of ADDSYM, above.

! A lexical environment is a list of Association Lists
! (Alists):
!
! (Alist1 Alist2 ...)
!
! A new Alist is added whenever arguments are bound to
! variables in a function call. Each value field of the
! Alist is a list of values, one for each namespace:
!
! ((SYMBOL . (VALUE FUNCTION-VALUE)) ... )
!
! Basically each association in the Alist of a lexical
! environment has the same structure as an interned symbol,
! but with a complete symbol in the CAR fielf instead of
! the characters of the symbol name:
!
!                   Value Box     Function Box
!   +---+---+       +---+---+      +---+---+
!   | O | O-------> | O | O------> | O |NIL|
!   +-|-+---+       +-|-+---+      +-|-+---+
!     |               |              |
!     V               V              V
!   SYMBOL          Value          Value

lookup(x, fbox) do var a ,e;
	e := Env;
	while (e \= NIL) do
		a := Car[e];
		while (a \= NIL) do
			if (caar(a) = x) do
				a := cdar(a);
				if (fbox) a := Cdr[a];
				return a;
			end
			a := Cdr[a];
		end
		e := Cdr[e];
	end
	a := Cdr[x];
	if (fbox) a := Cdr[a];
	return a;
end

! These functions return the Value Box
! and Function Box of a symbol:

valbox(x) return lookup(x, 0);
funbox(x) return lookup(x, 1);

! *******************************
! ***** S-EXPRESSION READER *****
! *******************************

! Keep track of open parentheses.

var	Parens;

! The list reader will have to read S-expressions
! and the S-expression reader will read lists, so
! they are mutually recursive.

decl read(0);

! RDLIST reads a list. It recurses through READ to read
! every member of the list, so it also reads nested lists.
! There are special cases for the dotted pair (X . Y) and
! the empty list (). In a pair the procedure makes sure
! that the pair is valid. Malformed pairs like ( . X) and
! (X . ) will be reported. NIL is returned for ().

! The function uses the append pointer A to append elements
! to the result list. The append pointer will always point
! to the most recently added element:
!
!                    A
!                    |
!                    V
! +---+---+      +---+---+      +---+---+
! | O | O------> | O | O------> |NIL|NIL|
! +-|-+---+      +-|-+---+      +---+---+
!   |              |
!   V              |
! +---+          +---+
! | X |          | Y |
! +---+          +---+

! When adding a new member, A will move to CDR[A], the
! new element will be inserted at CAR[A], and a fresh pair
! of the form (NIL . NIL) will be attached to CDR[A].
!
! Only when inserting the first element, A is NIL and
! will move to LST before inserting the element. See the
! code for details.

rdlist() do var n, lst, a, badpair;
	badpair := "bad pair";
	Parens := Parens+1;
	lst := cons(NIL, NIL);
	save(lst);
	a := NIL;
	while (%1) do
		if (Errflag) return NIL;
		n := read();
		if (n = EOT) return error("missing ')'", UNDEF);
		if (n = DOT) do
			if (a = NIL) return error(badpair, UNDEF);
			n := read();
			Cdr[a] := n;
			if (n = RPAREN \/ read() \= RPAREN)
				return error(badpair, UNDEF);
			unsave(1);
			Parens := Parens-1;
			return lst;
		end
		if (n = RPAREN) leave;
		ie (a = NIL) 
			a := lst;
		else
			a := Cdr[a];
		Car[a] := n;
		Cdr[a] := cons(NIL, NIL);
	end
	Parens := Parens-1;
	if (a \= NIL) Cdr[a] := NIL;
	unsave(1);
	return a = NIL-> NIL: lst;
end

! These characters can appear in a symbol name.
! Note that '\\' is an escape character and not
! really a symbolic character.

symbolic(c) return c >= 'a' /\ c <= 'z' \/
		   c >= 'A' /\ c <= 'Z' \/
		   c >= '0' /\ c <= '9' \/
		   c  = '-' \/ c  = '\\' \/
		   c  = '*' \/ c  = '&';

! The following variables refer to symbols naming
! the special operators of MICRO COMMON LISP. E.g.,
! S_lambda refers to the symbol LAMBDA.

! Other symbols included here that are not standard
! Common LISP symbols are FSETQ, IFNOT, and MACRO.
! S_closure is an internal marker for closures.
! These will be discussed later.

var	S_fsetq, S_function, S_if, S_ifnot, S_lambda,
	S_closure, S_macro, S_progn, S_quote, S_quasiquote,
	S_unquote, S_unquote_splice, S_setq;

! These variables refer to the names of built-in LISP
! functions and other symbols. S_t is T, S_restarg
! is &REST.

var	S_t, S_restarg, S_apply, S_cons, S_car, S_cdr, S_atom,
	S_eq, S_funcall, S_gensym, S_last, S_suspend, S_gc, S_eofp,
	S_load, S_macro_function, S_rplaca, S_rplacd, S_read,
	S_princ, S_terpri, S_error, S_symbols;

! RDSYM reads the characters of a symbol and returns
! an interned symbol. When reading the string "NIL",
! it will return NIL, which is a special value in this
! implementation. Symbol names are folded to upper case.

! Symbols can contain non-symbolic characters when
! escaped with a backslash. Escaped letters are not
! converted to upper case, so FOO is different from
! F\o\o.

rdsym(c) do var s::SYMLEN+1, i;
	i := 0;
	while (symbolic(c)) do
		if (c = '\\') c := rdch();
		ie (SYMLEN = i)
			error("long symbol", UNDEF);
		else if (i < SYMLEN) do
			s::i := c;
			i := i+1;
		end
		c := rdchci();
	end
	s::i := 0;
	Rejected := c;
	if (strequ(s, "NIL")) return NIL;
	return addsym(s, 0);
end

! Report a syntax error. Return zero.

syntax(x) error("syntax", x);

! Return a fresh expression of the form (Q N).
! Used to create quotations like (QUOTE N) or
! (UNQUOTE N), but also forms like (FUNCTION N).

quote(q, n) return cons(q, cons(n, NIL));

! Read a pseudo-string. A pseudo-string is an
! uninterned symbol that consists of exactly the
! characters between two quote (") characters.
! It is intended for use with LOAD and SUSPEND.

rdstr() do var c, s::SYMLEN+1, i;
	i := 0;
	c := rdch();
	while (c \= '"') do
		ie (SYMLEN = i)
			error("long symbol", UNDEF);
		else if (i < SYMLEN) do
			s::i := c;
			i := i+1;
		end
		c := rdch();
	end
	s::i := 0;
	return quote(S_quote, mksym(s));
end

! READ implements the LISP reader. It reads and returns
! a complete S-expression, which is either a symbol or
! a pair/list. The reader skips over whitespace characters
! and comments before reading an S-expression. Whitespace
! characters include the blank, CR, LF, and HT (tab)
! characters. A comment starts with a semicolon an extends
! up to the end of the current line. Because lists contain
! S-expressions, comments can appear inside of lists.

! The MICRO COMMON LISP reader accepts and expands the
! following abbreviations
!
!  'X  = (QUOTE X)
!  `X  = (QUASIQUOTE X)
!  ,X  = (UNQUOTE X)
! ,@X  = (UNQUOTE-SPLICE X)
! #'X  = (FUNCTION X)

! A percent character will be interpreted as the end of file.
! This is not compatible to Common LISP.

read() do var c;
	c := rdchci();
	while (%1) do
		while (c = ' ' \/ c = '\t' \/ c = '\n' \/ c = '\r') do
			if (Errflag) return NIL;
			c := rdchci();
		end
		if (c \= ';') leave;
		while (c \= '\n' /\ c \= '\r') c := rdch();
	end
	if (c = EOT \/ c = '%') return EOT;
	if (c = '(') do
		return rdlist();
	end
	if (c = '\'') do
		return quote(S_quote, read());
	end
	if (c = '`') do
		return quote(S_quasiquote, read());
	end
	if (c = ',') do
		c := rdchci();
		if (c = '@') return quote(S_unquote_splice, read());
		Rejected := c;
		return quote(S_unquote, read());
	end
	if (c = '#') do
		c := rdch();
		if (c \= '\'') syntax(UNDEF);
		return quote(S_function, read());
	end
	if (c = ')') do
		if (\Parens) error("extra paren", UNDEF);
		return RPAREN;
	end
	if (c = '.') do
		if (\Parens) error("free dot", UNDEF);
		return DOT;
	end
	if (symbolic(c)) do
		return rdsym(c);
	end
	if (c = '"') do
		return rdstr();
	end
	syntax(UNDEF);
	return UNDEF;
end

! ********************************
! ***** S-EXPRESSION PRINTER *****
! ********************************

! An Atom is everything that is not a list. In MICRO COMMON LISP
! this includes all symbols (interned or uninterned) as well as
! all special nodes, like NIL, the EOT, of the undefined value.

! Note, again, that a symbol is a CONS whose CAR field has the
! ATOMB bit set!

! ATOMP returns truth, if N is an atom.

atomp(n) return n >= SPCL \/ Car[n] < SPCL /\ Tag::Car[n] & ATOMB;

! ATOMIC is like ATOMP, but also recognizes functions as atoms,
! which are conses internally. It is mostly used for type checking.

atomic(n) return atomp(n) \/ Car[n] = S_closure;

! SYMBOLP returns truth, if N is a symbol. This excludes all
! special nodes.

symbolp(n) return n < SPCL /\ Car[n] < SPCL /\ Tag::Car[n] & ATOMB;

! PRINT2 prints the S-expression N. The parameter D is used
! to keep track of the depth of a list while printing. When
! the depth exceeds the PRDEPTH constant, printing is aborted,
! assuming that the printed structure is cyclic.

! PRINT2 will print <EOT> for the special value EOT and
! <FUNCTION> for function objects, which have the internal
! form (<CLOSURE> ...). It will print <UNPRINTABLE> for
! objects that it cannot print.

print2(n, d) do
	if (d > PRDEPTH) error("print depth", UNDEF);
	if (Errflag) return error("stop", UNDEF);
	ie (n = NIL) do
		pr("NIL");
	end
	else ie (n = EOT) do
		pr("<EOT>");
	end
	else ie (n >= SPCL \/ Tag::n & ATOMB) do
		pr("<UNPRINTABLE>");
	end
	else ie (symbolp(n)) do
		pr(symstr(n));
	end
	else do	! List
		if (\atomp(n) /\ Car[n] = S_closure) do
			pr("<FUNCTION>");
			return;
		end
		pr("(");
		while (n \= NIL) do
			print2(Car[n], d+1);
			n := Cdr[n];
			ie (symbolp(n)) do
				pr(" . ");
				print2(n, d+1);
				n := NIL;
			end
			else if (n \= NIL) do
				pr(" ");
			end
		end
		if (\Errflag) pr(")");
	end
end

! PRINT is the LISP printer.

print(n) print2(n, 0);

! **************************
! ***** IMAGE FILE I/O *****
! **************************

! The magic identifier identifying a
! MICRO COMMON LISP image file.
! Will be initialized in INIT.

var	Magic;

! The number to be appended to GENSYM symbols.
! Needs to be stored in the image file, so no
! duplicates will be generated.

var	Id;

! Write buffer B of length K to file FD.
! Report an error when the write fails.

dowrite(fd, b, k)
	if (t.write(fd, b, k) \= k)
		error("image write error", UNDEF);

! SUSPEND writes a LISP image to the file S.
! The image consists of the following parts:
!
!           OFFSET       SIZE   DESCRIPTION
! +---------------------------------------------------------+
! |              0 |        4 | Magic identifier            |
! |              4 |        2 | Number of nodes in the pool |
! |              6 |        2 | Bytes per machine word (W)  |
! |              8 |        2 | Address of the Freelist     |
! |             10 |        2 | Address of the Symbols list |
! |             12 |        2 | Current GENSYM id           |
! |             14 | W*NNODES | CAR fields of the node pool |
! |  W*NNODES + 14 | W*NNODES | CDR fields of the node pool |
! | 2W*NNODES + 14 |   NNODES | TAG fields of the node pool |
! +----------------+----------+-----------------------------+
! | 2W*NNODES      |          |                             |
! |  + NNODES + 14 |          | End of file / total size    |
! +---------------------------------------------------------+
!
! W = machine word size
! All numbers and addresses in the header are in Big Endian order.
! All addresses in the CAR and CDR pools are in native order.

suspend(s) do var fd, k, buf::20;
	fd := t.open(s, T3X.OWRITE);
	if (fd < 0) error("suspend", mksym(s));
	k := strlen(Magic);
	t.memcopy(buf, Magic, k);
	buf::(k) := (NNODES >> 8);
	buf::(k+1) := NNODES;
	buf::(k+2) := 0;
	buf::(k+3) := t.bpw();
	buf::(k+4) := (Freelist >> 8);
	buf::(k+5) := Freelist;
	buf::(k+6) := (Symbols >> 8);
	buf::(k+7) := Symbols;
	buf::(k+8) := (Id >> 8);
	buf::(k+9) := Id;
	dowrite(fd, buf, k+10);
	dowrite(fd, Car, NNODES * t.bpw());
	dowrite(fd, Cdr, NNODES * t.bpw());
	dowrite(fd, Tag, NNODES);
	t.close(fd);
end

! Read K bytes from file FD into buffer B.
! Report an error when fewer than K bytes were read.

doread(fd, b, k)
	if (t.read(fd, b, k) \= k)
		fatal("image read error");

! FASLOAD loads an image file. When the image cannot be
! opened, it returns silently. In this case the interpreter
! will run in a minimal configuration that is sufficient
! for bootstrapping the LISP system.

! The image file S must have a valid Magic ID, the
! same machine word size as the LISP system, and the
! same node pool size. It should probably also check
! endianness, but currently does not.

fasload(s) do var fd, k, n, m, buf::20;
	fd := t.open(s, T3X.OREAD);
	if (fd < 0) return;
	k := strlen(Magic);
	doread(fd, buf, k+10);
	n := buf::k << 8 | buf::(k+1);
	m := buf::(k+2) << 8 | buf::(k+3);
	if (n \= NNODES \/ m \= t.bpw() \/ t.memcomp(buf, Magic, k))
		fatal("bad image");
	Freelist := buf::(k+4) << 8 | buf::(k+5);
	Symbols := buf::(k+6) << 8 | buf::(k+7);
	Id := buf::(k+8) << 8 | buf::(k+9);
	doread(fd, Car, NNODES * t.bpw());
	doread(fd, Cdr, NNODES * t.bpw());
	doread(fd, Tag, NNODES);
	t.close(fd);
end

! ***********************************
! ***** BUILT-IN LISP FUNCTIONS *****
! ***********************************

! CHECK checks the parameters of a function call.
!
! X is the S-expression of the function call.
! K0 is the minimum number of expected arguments + 1
! KN is the maximum number of expected arguments + 1.
! When KN is -1, there is no upper limit.
!
! When the number of arguments is not in the expected
! range, an error is reported.

check(x, k0, kn) do var i, a;
	i := 0;
	a := x;
	while (a \= NIL) do
		i := i+1;
		a := Cdr[a];
	end;
	if (i < k0 \/ (kn \= %1 /\ i > kn))
		syntax(x);
end

! The number of nested LOAD calls.

var	Loads;

! Input buffer for LOAD. Each time LOAD is called,
! another segment of the buffer is used:
!
! +-----...-----+-----...-----+ ... +-----...-----+
! |             |             |     |             |
! +-----...-----+-----...-----+ ... +-----...-----+
!  First LOAD    Second LOAD         NLOAD'th LOAD

var	Tmpbuf::NLOAD*BUFLEN;

! LOAD needs EVAL.

decl eval(1);

! LOAD saves all parameters of the buffered input mechanism,
! then resets them and allocates a new buffer from TMPBUF
! above. It then reads expressions from its imput file (S)
! and evaluates them. It stops when the end of the input
! file is reached or an evaluation fails. Before exiting
! it restores the original parameters for input buffering,
! so that input will be read from the previous source again.

! The file name S is converted to all-lowercase.

load(s) do var fd, ib, ip, ik, in, re;
	if (Loads >= NLOAD) return error("load limit", UNDEF);
	fd := t.open(s, T3X.OREAD);
	if (fd < 0) return error("load", mksym(s));
	in := Input;
	ip := Inp;
	ik := Ink;
	ib := Inbuf;
	re := Rejected;
	Input := fd;
	Inbuf := @Tmpbuf::(Loads*BUFLEN);
	Inp := 0;
	Ink := 0;
	Rejected := EOT;
	Loads := Loads+1;
	Line := 1;
	while (\Errflag) do
		Acc := read();
		if (Acc = EOT) leave;
		eval(Acc);
	end
	Line := 0;
	t.close(fd);
	Loads := Loads-1;
	Rejected := re;
	Inbuf := ib;
	Ink := ik;
	Inp := ip;
	Input := in;
end

! Signal a type error in the expression X, return NIL;

type(x) return error("type", x);

! BUILTIN evaluates calls of built-in functions
! and returns their values. The patterns is similar
! for all functions:
!
! - check the number of arguments
! - check the types of the arguments
! - handle any special cases
! - apply the function and return its value
!
! Type checking is skipped, if the function has
! no limited domain, like CONS, PRINT, EQ, etc.

! Functions implemented here include the following
! standard Common LISP functions:
!
! CAR, CDR, EQ, ATOM, CONS, RPLACA, RPLACD, GENSYM,
! MACRO-FUNCTION, READ, PRIN1, TERPRI, LOAD.
!
! They appear in this order so that fast and
! frequently-used function will be identified early.

! BUILTIN also implements the following non-standard
! functions:
!
! (EOFP X)     Return T, if X is the EOT object.
! (ERROR M X)  Abort computation with error message M
!              and optional argument X.
! (GC)         Perform a garbage collection and return
! (GC T/NIL)   the number of free nodes as an uninterned
!              symbol. Optionally enable or disable
!              verbose garbage collections.
! (SUSPEND S)  Write image to file S.
! (SYMBOLS)    Return the list of interned symbols.

! Also note that built-in functions are just symbols.
! (FUNCTION X) ==> X for any built-in function symbol.

builtin(x) do var s, n;
	if (S_car = Car[x]) do
		check(x, 2, 2);
		n := cadr(x);
		if (n = NIL) return NIL;
		if (atomic(n)) return type(x);
		return Car[n];
	end
	if (S_cdr = Car[x]) do
		check(x, 2, 2);
		n := cadr(x);
		if (n = NIL) return NIL;
		if (atomic(n)) return type(x);
		return Cdr[n];
	end
	if (S_eq = Car[x]) do
		check(x, 3, 3);
		return cadr(x) = caddr(x)-> S_t: NIL;
	end
	if (S_atom = Car[x]) do
		check(x, 2, 2);
		return atomic(cadr(x))-> S_t: NIL;
	end
	if (S_cons = Car[x]) do
		check(x, 3, 3);
		return cons(cadr(x), caddr(x));
	end
	if (S_rplaca = Car[x]) do
		check(x, 3, 3);
		if (atomic(cadr(x))) return type(x);
		Car[cadr(x)] := caddr(x);
		return cadr(x);
	end
	if (S_rplacd = Car[x]) do
		check(x, 3, 3);
		if (atomic(cadr(x))) return type(x);
		Cdr[cadr(x)] := caddr(x);
		return cadr(x);
	end
	if (S_gensym = Car[x]) do
		check(x, 1, 1);
		Id := Id+1;
		s := ntoa(Id);
		s := s-1;
		s::0 := '#';
		return addsym(s, 0);
	end
	if (S_macro_function = Car[x]) do
		check(x, 2, 2);
		n := cadr(x);
		if (\symbolp(n)) return type(x);
		n := Car[funbox(n)];
		if (n = UNDEF \/ Car[n] \= S_macro) return NIL;
		return Cdr[n];
	end
	if (S_eofp = Car[x]) do
		check(x, 2, 2);
		return cadr(x) = EOT-> S_t: NIL;
	end
	if (S_read = Car[x]) do
		check(x, 1, 1);
		return read();
	end
	if (S_princ = Car[x]) do
		check(x, 2, 2);
		print(cadr(x));
		return cadr(x);
	end
	if (S_terpri = Car[x]) do
		check(x, 1, 1);
		nl();
		return S_t;
	end
	if (S_load = Car[x]) do
		check(x, 2, 2);
		if (\symbolp(cadr(x))) return type(x);
		load(symstr(cadr(x)));
		return S_t;
	end
	if (S_error = Car[x]) do
		check(x, 2, 3);
		if (\symbolp(cadr(x))) return type(x);
		ie (cddr(x) = NIL)
			error(symstr(cadr(x)), UNDEF);
		else
			error(symstr(cadr(x)), caddr(x));
		return UNDEF;
	end
	if (S_gc = Car[x]) do
		check(x, 1, 2);
		if (Cdr[x] \= NIL)
			Verbose_GC := cadr(x) \= NIL;
		return mksym(ntoa(gc()));
	end
	if (S_suspend = Car[x]) do
		check(x, 2, 2);
		if (\symbolp(cadr(x))) return type(x);
		suspend(symstr(cadr(x)));
		return S_t;
	end
	if (S_symbols = Car[x]) do
		check(x, 1, 1);
		return Symbols;
	end
	syntax(x);
	return UNDEF;
end

! **************************************
! ***** BUILT-IN SPECIAL OPERATORS *****
! **************************************

! Return truth, if N is a symbol denoting a
! built-in special operator. Again, ordered
! for fast lookup.

specialp(n)
	return	n = S_quote \/
		n = S_if \/
		n = S_progn \/
		n = S_ifnot \/
		n = S_lambda \/
		n = S_function \/
		n = S_closure \/
		n = S_setq \/
		n = S_fsetq;

! The evaluator is always in one of the following
! modes/states:

! MHALT - halt evaluation, return result
! MEXPR - start evaluation of expression
! MLIS1 - evaluate function of function call
! MLIST - evaluate arguments of function call
! MCALL - perform function call
! MRETN - return from function call
! MPRED - evaluate predicate of IF
! MNOTP - evaluate predicate of IFNOT
! MPROG - evaluate body of PROGN
! MSETQ - evaluate value of SETQ
! MFSET - evaluate value of FSETQ

struct MODES =	MHALT, MEXPR, MLIS1, MLIST, MCALL, MRETN,
		MPRED, MNOTP, MPROG, MSETQ, MFSET;

! Save (push) current state on MSTACK.

msave(n) Mstack := cons3(n, Mstack, ATOMB);

! Restore (pop) saved state from MSTACK.

munsave() do var n;
	if (Mstack = NIL) fatal("mstack empty");
	n := Car[Mstack];
	Mstack := Cdr[Mstack];
	return n;
end

! Check the syntax of the &REST argument. There must
! be exactly one argument after &REST. I.e. (&REST) is
! wrong, and so is (&REST X Y).

ckrest(p, x) do
	if (Cdr[p] = NIL \/ cddr(p) \= NIL) return syntax(x);
	return %1;
end

! Check the syntax of LAMBDA. There must be at least two
! arguments and the first one, the argument list, must be
! a list of symbols. The argument list may contain one
! &REST keyword, as described above.

! The argument list may be dotted, where the variable
! after the dot serves as a &REST argument. This is *not*
! valid Common LISP syntax, but MICRO COMMON LISP uses
! this notation internally. When CKLAM finds a &REST
! keywords, it replaces the cons cell of &REST with the
! argument following &REST.

! The transformations
!   (&REST X) -> X       and
! (X &REST Y) -> (X . Y)
! are performed in-situ, which is why CKLAM also has to
! accept the dotted list syntax.

! Another side effect is that dotted argument lists may
! appear in error messages.

! This could be fixed by making a flat copy of the LAMBDA
! expression and replacing the argument list in the copy.

cklam(x) do var p, q;
	check(x, 3, %1);
	p := cadr(x);
	if (Car[p] = S_restarg) do
		if (ckrest(p, x)) Car[Cdr[x]] := cadr(p);
		return;
	end
	while (\atomp(p)) do
		if (\symbolp(Car[p])) syntax(x);
		if (Car[p] = S_restarg) do
			if (ckrest(p, x)) Cdr[q] := cadr(p);
			return;
		end
		q := p;
		p := Cdr[p];
	end
end

! Create a function/closure from the lambda form X.
! A function has the internal form
!
! (<CLOSURE> ENV (ARG ...) EXPR ...)
!
! where ENV is the lexical environment of the function
! and <CLOSURE> is an internal symbol.

mkfun(x) return cons(S_closure, cons(Env, Cdr[x]));

! Report an unbound symbol.

undefd(x) return error("undefined", x);

! SPECIAL evaluates special forms and returns their
! values. Its internal mechanism is similar to the
! BUILTIN procedure, but in addition it can modify
! the evaluator mode (PM[0]) and the MSTACK.

! X is a function application with its arguments
! unevaluated. PM is the address of the evaluator mode,
! so changing PM[0] will change the mode.

! SPECIAL can return expressions that need evaulation.
! For example, in the evaluation of (IF P X Y), the
! nest step would be the evaluation of the predicate
! P, so SPECIAL would set the evaluator mode to MEXPR
! and return P. It would also push MPRED to the MSTACK,
! so the evaluator will continue the evaluation of the
! IF form as soon as the value of P is known.

! When SPECIAL returns an expression to evaluate, it
! will typically also store other expressions on the
! stack for later evaluation. For example, when
! evaluating (IF P X Y), it will push (X Y) to the
! stack, push MPRED to the MSTACK, set the mode to
! MEXPR and return P.
! The evaluator will then evaluate P, and when it is
! done it will pop MPRED from the MSTACK. In MPRED
! mode, it will check the value of P (now in ACC) and
! then select either X or Y from the stack and go back
! to MEXPR mode in order to evaluate it.

! In many other cases SPECIAL will just return a value,
! like in QUOTE, FUNCTION, LAMBDA, etc. In these cases
! it will immediately restore the previous mode from
! the MSTACK, because the evaluation of the special
! form is complete.

! The FSETQ and IFNOT special forms are not Common LISP
! forms.
!
! (FSETQ X Y)    is like SETQ, but modifies the function
! (FSETQ X Y T)  box of a symbol. When a third argument
!                is given (no matter which), it always
!                modified the global function box of F.
! (IFNOT X Y)    evalutes to X, if X it not NIL and
!                otherwise to Y. X is never evaluated
!                twice, so IFNOT can, for example,
!                implement OR without using LET.

special(x, pm) do
	if (S_quote = Car[x]) do
		check(x, 2, 2);
		pm[0] := munsave();
		return cadr(x);
	end
	if (S_if = Car[x]) do
		check(x, 4, 4);
		msave(MPRED);
		pm[0] := MEXPR;
		save(cddr(x));
		return cadr(x);
	end
	if (S_progn = Car[x]) do
		pm[0] := MEXPR;
		if (Cdr[x] = NIL) return NIL;
		if (cddr(x) = NIL) return cadr(x);
		msave(MPROG);
		save(cddr(x));
		return cadr(x);
	end
	if (S_ifnot = Car[x]) do
		check(x, 3, 3);
		msave(MNOTP);
		pm[0] := MEXPR;
		save(caddr(x));
		return cadr(x);
	end
	if (S_lambda = Car[x]) do
		cklam(x);
		pm[0] := munsave();
		return mkfun(x);
	end
	if (S_function = Car[x]) do var n, m;
		n := cadr(x);
		pm[0] := munsave();
		ie (S_lambda = Car[n]) do
			cklam(n);
			return mkfun(n);
		end
		else do
			m := Car[funbox(n)];
			if (m = UNDEF \/ Car[m] = S_macro)
				undefd(n);
			return m;
		end
	end
	if (S_closure = Car[x]) do
		pm[0] := munsave();
		return x;
	end
	if (S_setq = Car[x] \/ S_fsetq = Car[x]) do
		check(x, 3, 4);
		if (\symbolp(cadr(x))) syntax(x);
		msave(Car[x] = S_setq-> MSETQ: MFSET);
		pm[0] := MEXPR;
		save(Cdr[x]);
		return caddr(x);
	end
	syntax(x);
	return UNDEF;
end

! **************************
! ***** MACRO EXPANDER *****
! **************************

! NREVERSE reverses a list in situ and returns the
! reversed list. Note that just doing nreverse(X) will
! not reverse X! This is because the head of the
! original list will become the tail of the new list:
!
!     X = (A B C D E)
!          |
!          V
! (E D C B A)
!
! So after nreverse(X), X will be (A). In order to
! make X point to the reversed list, the statement
!
! X := nreverse(X);
!
! has to be used. The same caveat applies to the LISP
! function NREVERSE.

nreverse(n) do var m, h;
	m := NIL;
	while (n \= NIL) do
		h := Cdr[n];
		Cdr[n] := m;
		m := n;
		n := h;
	end
	return m;
end

! Count nested macro expansion.

var	Exlev;

! EXPAND expands all macros in the expression X.

! A macro is an object of the form (MACRO . <FUNCTION>)
! that is bound in the function box of a symbol.
! <FUNCTION> is the macro expander of the macro. It
! converts the macro application to its expanded form.

! If the M in an expression X = (M A B ...) is a macro,
! EXPAND will create a new expression of the form
!
! (APPLY EXP `(QUOTE ,(CDR X)))
!
! where EXP is the macro-expander associated with the
! macro M.

! EXPAND then submits the expression
! (APPLY EXP `(QUOTE ,(CDR X)))
! for evaluation and expands the returned value again
! to expand any macros that are left in the expanded
! form or have been introduced during expansion.

! If the M in X = (M A B ...) is not a macro, EXPAND
! will expand every member of the list (M A B ...).

! If the expression to expand is a lambda function,
! EXPAND will expand every expression in the body of
! the function, but will skip over the keyword LAMBDA
! and the argument list.

! EXPAND returns a new expression with all macro
! applications expanded.

! EXPAND will not recurse more than EXDEPTH times,
! mostly in order to avoid stack overflows due to
! infinte expansion.

! Macro expansion costs 12 bytes per recursion level,
! so with the default EXDEPTH, it will stop at a
! stack depth of 12 * 128 = 1536 bytes.

expand(x) do var n, m, skip;
	if (atomp(x)) return x;
	if (Car[x] = S_quote) return x;
	if (Exlev >= EXDEPTH)
		return error("expansion limit", UNDEF);
	Exlev := Exlev + 1;
	n := symbolp(Car[x])-> Car[funbox(Car[x])]: UNDEF;
	if (n \= UNDEF /\ Car[n] = S_macro) do
		m := quote(S_quote, Cdr[x]);
		m := cons(m, NIL);
		m := cons(Cdr[n], m);
		m := cons(S_apply, m);
		save(m);
		n := eval(m);
		Car[Stack] := n;
		n := expand(n);
		unsave(1);
		Exlev := Exlev - 1;
		return n;
	end
	m := x;
	while (\atomp(m)) m := Cdr[m];
	if (symbolp(m)) do
		Exlev := Exlev - 1;
		return x;
	end
	save(x);
	n := NIL;
	save(n);
	skip := Car[x] = S_lambda-> 2: 0;
	while (x \= NIL) do
		m := skip-> Car[x]: expand(Car[x]);
		n := cons(m, n);
		Car[Stack] := n;
		x := Cdr[x];
		if (skip) skip := skip-1;
	end
	n := nreverse(unsave(1));
	unsave(1);
	Exlev := Exlev - 1;
	return n;
end

! *********************
! ***** EVALUATOR *****
! *********************

! BINDARGS creates a pair of new binding boxes for each
! variable in the list V and stores the corresponding
! argument value from the list A in its value box. The
! function box of each variable will be set to UNDEF
! (undefined).

! If V is a NIL-terminated list, there must be exactly
! one argument for each variable, i.e. the lists V and
! A must have the same length.

! If V is a dotted list, the list A may be longer than
! the list V. In this case a list of all extra arguments
! will be bound to the symbol after the dot.

! All new bingings created by BINDARGS will be put in
! an Alist and the new Alist will then be consed to the
! lexical environment Env. Hence the bindings in the new
! Alist will override or shadow all existing bindings in
! Env.

! An error is reported when there are too few or too
! many arguments in A.

bindargs(v, a) do var e, n;
	e := NIL;
	save(e);
	while (\atomp(v)) do
		if (a = NIL) return error("too few args", Acc);
		n := cons(Car[v], mkboxes(Car[a], UNDEF));
		e := cons(n, e);
		Car[Stack] := e;
		v := Cdr[v];
		a := Cdr[a];
	end
	ie (symbolp(v)) do
		n := cons(v, mkboxes(a, UNDEF));
		e := cons(n, e);
		Car[Stack] := e;
	end
	else if (a \= NIL) do
		return error("extra args", Acc);
	end
	Env := cons(e, Env);
	unsave(1);
end

! FUNCALL performs a call to a lambda function. Calls
! to built-in functions will be handled by the BUILTIN
! procedure discussed earlier.

! A function is called by
!
! - saving the current lexical environment (Env)
! - fetching a new lexical environment from the function
! - binding the function arguments to the function's
!   variables
! - saving the MRETN mode on the MSTACK, which will
!   later restore the saved environment
! - returning the body of the function for evaluation
!
! Because the body may consist of multiples expressions,
! a PROGN form is wrapped around the body.

! FUNCALL also performs tail call optimization (TCO).
! If the MRETN mode is currently on the top of the
! MSTACK, the current function call is a tail call. In
! this case the current environment does not need to
! be saved or restored later.
! This optimization basically turns a function call
! into a jump to the function. It makes sure that
! tail-recursive programs evaluate in constant space.
!
! This is important, because MICRO COMMON LISP uses
! tail recursion to implement loops.

! The layout of the components in the function call
! is as follows:
!
! X = ((<CLOSURE> ENV VARS BODY ...) ARG ...)
!
!     [o|o]--->[o|o]---> ...
!      |        |
!      |        V
!      |       ARG
!      V
!    [o|o]--->[o|o]--->[o|o]---> BODY
!     |        |        |
!     V        V        V
! <CLOSURE>   ENV      VARS
!
! So: (CADAR X)  is ENV
!     (CADDAR X) is VARS
!     (CDDDAR X) is BODY

funcall(x) do
	if (atomp(Car[x]) \/ caar(x) \= S_closure)
		syntax(x);
	ie (Mstack \= NIL /\ Car[Mstack] = MRETN) do
		Env := cadar(x);
		bindargs(caddar(x), Cdr[x]);
	end
	else do
		save(Env);
		Env := cadar(x);
		bindargs(caddar(x), Cdr[x]);
		msave(MRETN);
	end
	return cons(S_progn, cdddar(x));
end

! EVAL evaluates the expression X and returns its value.

! EVAL is a push-down state machine with eleven states
! enumerated earlier in the MODES struct. It starts with
! MHALT on the mode stack so that it will halt after
! evaluating X. Its initial mode is MEXPR. While an
! expression evaluates, its intermediate results are
! kept in the Accumulator ACC.

! In MEXPR mode EVAL evaluates symbols to their values
! and special nodes (like NIL) to themselves. It applies
! special operators directly. When a function call is
! found in MEXPR mode, the evaluator saves the tail of
! the list as well as NIL on the stack and loads ACC
! with the head of the list. This causes the head of
! the list to be evaluated in the next iteration. It
! also pushes MLIS1 to the MSTACK.

! In MLIS1 and MLIST mode, the result in ACC is consed
! to the list on top of the stack, and the next element is
! extracted from the second element on the stack. After
! evaluating the first list element in MLIS1 mode, the
! evaluator switches to MLIST mode for the remaining
! elements.

! The difference between the two modes is that only
! symbols and LAMBDA expressions will be accepted in MEXPR
! mode when MLIS1 is on top of the MSTACK and values of
! symbols will be looked up in their function boxes. When
! MLIST (or anything else, in fact) is on top of the MSTACK,
! all kinds of expressions are accepcted and values of
! symbols are looked up in their value boxes.

! The evaluator oscillates between MLIST and MEXPR mode
! until all elements of list have been evaluated. When the
! source list (second stack element) is empty, it reverses
! the result list (top of stack) in situ and switches to
! MCALL mode.

! In MCALL mode the evaluator performs a function call.
! If the first element of the evaluated list is still a
! symbol, it runs a built-in function and otherwise it
! performs a lambda function call. A built-in function
! returns a value immediately, so the evaluator switches
! back to the previous mode. The FUNCALl function returns
! a function body, so EVAL switches to MEXPR mode to
! evaluate the body.

! FUNCALL also pushes the MRETN mode (except in a tail call),
! so after evaluating the body, the evaluator enters MRETN
! mode, in which it restores the lexical environment of the
! caller and returns to the previous mode.

! Other modes:

! MPRED mode processes IF expressions. Given an expression
! (IF P X Y), SPECIAL pushes MPRED to the MSTACK and (X Y)
! to the stack and returns P in MEXPR mode. After evaluating
! P, MPRED mode selects X or Y depending on the value of ACC.

! MNOTP is similar, but processes (IFNOT X Y).

! MSETQ and MFSET process the (SETQ X Y), (FSETQ X Y), and
! (FSETQ X Y T) special forms. In all cases SPECIAL sets up
! the evaluation of Y and pushes (X Y) [or (X Y T)] on the
! stack. The MSETQ or MFSET mode then stores the value of ACC
! in the corresponding box of X. If FSETQ has an additional
! argument, the value will be stored in the global function
! box of X, even if a local X exists.

! MPROG processes PROGN forms with at least two expressions.
! PROGN forms with fewer expressions are evaluated directly
! without pushing MPROG. MPROG simply evaluates the next
! expression from a list pushed by SPECIAL until there is
! only one expresion left. For this last expression MPROG
! switches to MEXPR mode without pushing MPROG again.

! MHALT returns ACC, thereby ending evaluation.

! Before evaluation starts, EVAL macro-expands the expression 
! passed to it. Macro expansion involves the recursive
! application of EVAL.

! Then there is one nice hack in EVAL. You may have noticed
! that the APPLY and FUNCALL functions are not implemented
! in the BUILTIN procedure. This is because both of them
! involve function calls and doing so inside of BUILTIN would
! break tail call optimization. So EVAL is making use of two
! simple observations: after evaluating the arguments of APPLY
! and FUNCALL,
!
! (FUNCALL F X ...)  ==  (F X ...)
!
! and
!
! (APPLY F (X ...))  ==  (CONS F (X ...))  ==  (F X ...)
!
! The effect of FUNCALL is to look up a function binding in
! the value/variable namespace. When evaluating all members
! of the function call (FUNCALL F ...), F is in the second
! slot, so it is automatically been looked up in the value
! namespace. So the FUNCALL symbol can simply be removed at
! this point.
!
! The effect of (APPLY F LIST) is to apply the function F
! to the arguments in LIST, so after evaluating the arguments
! F and LIST, F can simply be consed to LIST and the resulting
! function call can then be performed.
!
! EVAL performs these transformations in the MCALL state.
! It performs the transformtions in a loop to also allow
! for constructs like
! (APPLY #'FUNCALL ...) and (FUNCALL #'APPLY ...).

eval(x) do var n, m;
	Acc := x;
	if (\Exlev) Acc := expand(Acc);
	msave(MHALT);
	m := MEXPR;
	while (\Errflag) do
		ie (m = MEXPR) do
			if (\Batch /\ con.pollkey() = 3)
				error("stop", UNDEF);
			if (Car[Mstack] = MLIS1) do
				if (symbolp(Acc)) do
					n := Car[funbox(Acc)];
					if (n = UNDEF) undefd(Acc);
					Acc := n;
					m := munsave();
					loop;
				end
				if (Car[Acc] \= S_lambda) syntax(Acc);
			end
			ie (symbolp(Acc)) do
				n := Car[valbox(Acc)];
				if (n = UNDEF) undefd(Acc);
				Acc := n;
				m := munsave();
			end
			else ie (atomp(Acc)) do
				m := munsave();
			end
			else ie (specialp(Car[Acc])) do
				Acc := special(Acc, @m);
			end
			else do
				save(Cdr[Acc]);
				Acc := Car[Acc];
				save(NIL);
				msave(MLIS1);
			end
		end
		else ie (m = MLIS1 \/ m = MLIST) do
			n := cadr(Stack);
			ie (atomp(n)) do
				if (n \= NIL) syntax(UNDEF);
				Acc := nreverse(cons(Acc, unsave(1)));
				unsave(1);
				m := MCALL;
			end
			else do
				Car[Stack] := cons(Acc, Car[Stack]);
				Acc := caadr(Stack);
				Car[Cdr[Stack]] := cdadr(Stack);
				msave(MLIST);
				m := MEXPR;
			end
		end
		else ie (m = MCALL) do
			while (%1) do
				ie (Car[Acc] = S_funcall)
					Acc := Cdr[Acc];
				else ie (Car[Acc] = S_apply)
					Acc := cons(cadr(Acc), caddr(Acc));
				else
					leave;
			end
			ie (symbolp(Car[Acc])) do
				Acc := builtin(Acc);
				m := munsave();
			end
			else do
				Acc := funcall(Acc);
				m := MEXPR;
			end
		end
		else ie (m = MRETN) do
			Env := unsave(1);
			m := munsave();
		end
		else ie (m = MPRED) do
			n := unsave(1);
			ie (Acc = NIL)
				Acc := cadr(n);
			else
				Acc := Car[n];
			m := MEXPR;
		end
		else ie (m = MNOTP) do
			n := unsave(1);
			ie (Acc = NIL) do
				Acc := n;
				m := MEXPR;
			end
			else do
				m := munsave();
			end
		end
		else ie (m = MSETQ \/ m = MFSET) do
			n := unsave(1);
			ie (m = MSETQ)
				Car[valbox(Car[n])] := Acc;
			else ie (cddr(n) \= NIL)
				Car[cddar(n)] := Acc;
			else
				Car[funbox(Car[n])] := Acc;
			m := munsave();
		end
		else ie (m = MPROG) do
			ie (cdar(Stack) = NIL) do
				Acc := Car[unsave(1)];
				m := MEXPR;
			end
			else do
				Acc := caar(Stack);
				Car[Stack] := cdar(Stack);
				msave(MPROG);
				m := MEXPR;
			end
		end
		else ie (m = MHALT) do
			return Acc;
		end
		else do
			fatal("bad mode");
		end
	end
	return NIL;
end

! ***************************************
! ***** INITIALIZATION AND SHUTDOWN *****
! ***************************************

! RESET resets all reader and evaluator variables
! (and a few more). This will be done each time
! the REPL starts over.

! When the parameter Z is not zero, the ZNODE
! will be cleared. This is not possible when
! calling RESET for the first time, because
! the ZNODE has not yet been allocated.

! RESET is called with Z=0 to initialize the
! GC roots (with NIL) before running the first GC.

reset(z) do
	Exlev := 0;
	Parens := 0;
	Loads := 0;
	Errflag := 0;
	Line := 0;
	Acc := NIL;
	Env := NIL;
	Stack := NIL;
	Mstack := NIL;
	Tmpcar := NIL;
	Tmpcdr := NIL;
	if (z) do
		Car[Znode] := NIL;
		Cdr[Znode] := NIL;
	end
end

! INIT Initializes the system:
!
! - Set up the CONSOLE interface
!   (except when in Batch mode)
! - Set up the Magic ID
! - Clear the Symbol list and the Freelist
! - Clear the history buffers
! - Set the GENSYM ID to 0
! - Flush the input channel
! - Make garbage collection non-verbose
! - Clear all TAG bits so that no nodes
!   have their MARKB bit set
! - Allocate the Znode
! - Reset the evaluator state

! The rest of the procedure adds all kinds of symbols,
! like the names of the special operators (with UNDEF
! bindings, so they appear to be undefined) and the
! names of built-in function (which are bound to their 
! own names via the function box).

! The symbol T refers to itself through the value box.

! The console is set up for a VT100 terminal on CP/M.
! The CON.SETUP parameter is ignored on Unix and DOS.

init() do
	if (\Batch) do
		con.setup([LINES, "\e[H\e[J", "\e[K", "\e[%yd1;%xd1H",
			"\eM", "\e[?25l", "\e[?25h", "\e[7m", "\e[m"]);
		clear();
		if (COLUMNS > BUFLEN)
			fatal("screen too wide");
	end
	Magic := "MC06";
	Symbols := NIL;
	Freelist := NIL;
	Auxb1::0 := 0;
	Auxb2::0 := 0;
	Id := 0;
	flushinp();
	Verbose_GC := 0;
	t.memfill(Tag, 0, NNODES);
	reset(0);
	Znode := NIL;
	Znode := cons(NIL, NIL);
	S_t := addsym("T", 0); Car[valbox(S_t)] := S_t;
	S_restarg := addsym("&REST", 0);
	S_closure := addsym("<CLOSURE>", 0);
	S_macro := addsym("MACRO", 0);
	S_fsetq := addsym("FSETQ", 0);
	S_function := addsym("FUNCTION", 0);
	S_if := addsym("IF", 0);
	S_ifnot := addsym("IFNOT", 0);
	S_lambda := addsym("LAMBDA", 0);
	S_progn := addsym("PROGN", 0);
	S_quote := addsym("QUOTE", 0);
	S_quasiquote := addsym("QUASIQUOTE", 0);
	S_unquote := addsym("UNQUOTE", 0);
	S_unquote_splice := addsym("UNQUOTE-SPLICE", 0);
	S_setq := addsym("SETQ", 0);
	S_last := addsym("*", 0);
	S_apply := addsym("APPLY", %1);
	S_macro_function := addsym("MACRO-FUNCTION", %1);
	S_cons := addsym("CONS", %1);
	S_car := addsym("CAR", %1);
	S_cdr := addsym("CDR", %1);
	S_atom := addsym("ATOM", %1);
	S_eq := addsym("EQ", %1);
	S_eofp := addsym("EOFP", %1);
	S_funcall := addsym("FUNCALL", %1);
	S_rplaca := addsym("RPLACA", %1);
	S_rplacd := addsym("RPLACD", %1);
	S_gensym := addsym("GENSYM", %1);
	S_read := addsym("READ", %1);
	S_princ := addsym("PRINC", %1);
	S_terpri := addsym("TERPRI", %1);
	S_error := addsym("ERROR", %1);
	S_load := addsym("LOAD", %1);
	S_gc := addsym("GC", %1);
	S_suspend := addsym("SUSPEND", %1);
	S_symbols := addsym("SYMBOLS", %1);
	reset(1);
end

! Shut down and exit.

fini(x) do
	message("");
	if (\Batch) con.shutdown();
	ie (x) halt 1; else halt 0;
end

! ********************************
! ***** READ-EVAL-PRINT LOOP *****
! ********************************

! When the interpreter starts, it reads a single
! parameter from the command line. When it is a
! single minus sign (-), it enters Batch mode and
! otherwise it attempts to load an image file with
! the given name. When no parameter is given, it
! tries to load an image file named "mclisp".

! The REPL itself is simple:
!
! - reset the interpreter state
! - prompt and read an expression X
! - in case of a read error, loop
! - when X is the EOT, exit
! - evaluate the expression
! - in case of an error, loop
! - print the value of the expression
! - store the value in the variable * (S_last)
! - loop

! In interactive (non-batch) mode, the interpreter
! will announce its current state (READ, EVAL, PRINT)
! in the status line.

do var a::14, c;
	c := t.getarg(1, a, 14);
	Batch := 0;
	if (c > 0 /\ strequ(a, "-")) Batch := %1;
	init();
	fasload(c>0 /\ \Batch-> a: "mclisp");
	pr("MICRO COMMON LISP "); pr(@Magic::2); nl();
	if (\Batch) do
		pr(ntoa(gc()));
		pr(" NODES"); nl();
	end
	while (%1) do
		reset(1);
		message("[READ]");
		pr("*\s");
		Acc := read();
		if (Errflag) loop;
		if (Acc = EOT) leave;
		message("[EVAL]");
		Acc := eval(Acc);
		if (Errflag) loop;
		message("[PRINT]");
		print(Acc);
		nl();
		Car[valbox(S_last)] := Acc;
	end
	if (Batch) nl();
	fini(0);
end

contact | privacy