Back to index
Under the hood
This chapter describes some internals of LispMe. You don't need this to
work with LispMe, but some people asked me about the implementation, so
here you are:
General
The latest version 2.8 of LispMe was developed with GCC 2.95.2-kgpd,
prc-tools 2.0 and PilRC v2.5b3 on a PC running Windows NT
and tested with the Poser 3.0a2.
Altogether LispMe consists of about 14000 lines of highly compact C code.
All error handling is done using Pilot's ErrCatch and
ErrThrow macros/functions and not a single function from the
C library has been used.
The trick I found to create executables >32k code with GCC is fortunately
no more necessary prc-tools 2.0, since multi-segment applications are
supported now.
If you're interested in these
topics, have a look here.
I'm currently writing an article on LispMe's implementation, which
gives much more detail than this section.
LispMe is based on the SECD virtual machine described in
Peter Henderson
Functional Programming - Application and Implementation
Prentice Hall International
ISBN 0-13-331579-7
The name stems from the 4 main registers this machine operates on:
S | stack | used to hold intermediate values while
evaluating an expression
|
E | environment | used to hold the current environment
|
C | control | the machine-language program being
executed
|
D | dump | a stack the other registers are pushed on
when a new function is called
|
In fact, all registers hold pointers to list structures in the heap,
so for example going to the next machine instruction is accomplished
by an assignment of the form C = cdr(C);
The virtual machine currently has 155 different instructions, most of
them dealing with LispMe's primitive procedures. The main extensions
to Henderson's machine are
- error checking
- including function arity in closures
- closures with variable number of arguments
- support for tail-recursion
- continuation support
- support for sequencing (begin)
- real, complex, char, string, vector and port support
- compile quasiquote templates to SECD code
- support for macros and eval
The runtime environment is represented as a list of frames, where
each frame is a list of values. Variables are always accessed by
lexical adresses (a pair indicating the frame and the offset of the
variable). This is exactly the form you see when tapping the
names button. In fact, the first
step in each evaluation is assigning the value list to the E register.
In contrast to Henderson's approach, the compiler is not bootstrapped,
it's implemented in C like the rest of LispMe. The reasons for that are
compilation speed and saving heap memory for user programs.
A LispMe expression is compiled by traversing its tree structure and
consing up a list of machine instructions backwards to avoid using
append. There's a simple peephole optimizer mainly used for
removing unnecessary stack operations and for propagating returns
through conditionals to provide proper tail recursion elimination.
There's also a compile time environment (represented as a list of lists,
mirroring the structure of the runtime environment) to determine lexical
adresses for all variables. You can view the SECD code of a closure
with disasm.
Macros are expanded by calling the VM with the expansion procedure
and the original source expression as argument.
Parsing is divided into 2 steps:
- The scanner groups characters into tokens by using a
finite automaton consisting of 24 states. Most of the states deal
with real and complex numbers and escape sequences in strings.
- The parser is recursive-descent
Scanning and formatting floating point was particularly nasty, as the
Pilot ROM routines FlpFToA and FlpAToF are unusable
(limited precision, limited exponent range, no error support), so all
this stuff had to be written from scratch.
Each LispMe session is kept in a separate database bearing the same
name as the session. All LispMe session databases use creator ID
fbLM so that the total memory used by them is displayed correctly
in the memory application.
In each session database, at least 6 records are used:
- Record 0: Global vars, root pointers
- Record 1: The atom store; all identifier names are entered here by the
reader. The names are simply stored one after another separated by
'\0' bytes.
- Record 2: The floating point store; all floating point numbers are stored
here (8 bytes each). Unused cells are linked into a free list.
- Record 3: The heap; each heap cell is 32 bit and consists of two
pointers (car and cdr), 16 bit each. The cells do not
carry type tags, instead the pointers do according to this table:
sddd dddd dddd ddd1
| 15 bit signed integer (-16384 .. 16383)
|
sddd dddd dddd dd00
| 16 bit signed offset from virtual heap base,
which is 32k above actual heap begin (no scale neccessary)
|
dddd dddd dddd 0010
| unsigned 12 bit index into atom table
(shift right 4 bits), 4kb atom space
|
dddd dddd dddd 0110
| unsigned 12 bit index into double table
(shift right 1 bit and unmask 3 low bits), 4096 reals
|
aaaa aaaa 0000 1010
| 8 bit ASCII char
|
uuuu uuuu 0100 1110
| vector in heap, upper 8 bit of UID, cdr field
of this cells contains lower 16 bit (untagged!)
|
uuuu uuuu 0101 1110
| string in heap, upper 8 bit of UID, cdr field
of this cells contains lower 16 bit (untagged!)
|
xxxx xxxx 0011 1110
| reserved for special values
|
xxxx xxxx 0111 1110
| special value tags in car of list
|
Some special values are 0x013e for empty list,
0x023e for #f,
0x033e for #t,
0x007e for a closure tag or
0x067e for a complex tag (There are some more).
All pointers into the heap are relative pointers (offsets) to
a virtual heap base lying 32k behind the actual beginning of the
heap. This allows relocation of the heap by the
operating system. Additionally,
this saves memory (16 bit relative pointers vs. 32 bit absolute
pointers) and maps nicely to the 68000 adressing mode
adress register indirect with index and offset, which
uses signed offsets .
- Record 4: The input field
- Record 5: The output field
- Each vector and string occupies a single DB record starting with record number
6. A vector or string is identified by its
unique ID in a cons cell in the heap. To distinguish them,
strings are put into category 1 and vectors into category 0.
Starting with version 1.7, LispMe mo more uses the dynamic heap for
its memory, instead it works directly on DB blocks, gaining
write access via MemSemaphoreReserve. This is a much discussed
technique, as accessing memory out of its bounds can overwrite other
databases and even require a hard reset. On the other hand, LispMe has
been out for some months now and several hundred people have tested it and
I never got a report of an "invalid pointer access". Furthermore, there's
no way in LispMe to create an invalid pointer by will or accident (like
in other Scheme implementations, too). All
pointers are created in the memory subsystem which almost certainly
is bugfree. So I finally decided to use this technique to make a
bigger heap available on all Pilots.
LispMe uses mark/scan garbage collector for heap cells and floating
point cells. The reasons for preferring mark/scan to copying garbage
collection are:
- Memory usage has top priority on the Pilot. Wasting half of the
memory with copying GC isn't affordable.
- All heap cells have the same size, so fragmentation can't happen
- Heap compaction (provided by copying GC) doesn't affect performance,
as there's no virtual memory or cache on the Pilot.
- The disadvantage that mark/scan GC has to touch every memory
cell in the heap is ignorable with heap sizes possible on the Pilot
All in all, a typical garbage collection of 16k heap takes about
0.2 seconds.