|
Searching XEmacs
Quick Links
About XEmacs
Getting XEmacs
Customizing XEmacs
Troubleshooting XEmacs
Developing XEmacs
|
|
|
Implementation of a Lisp Engine Replacement
Owner: ???
Effort: ???
Dependencies: ???
Let's take a look at the sort of work that would be required if we
were to replace the existing Elisp engine in XEmacs with some other
engine, for example, the Clisp engine. I'm assuming here, of course,
that we are not going to be changing the interface here at the same
time, which is to say that we will be keeping the same Elisp language
that we currently have as the extension language for XEmacs, except
perhaps for incremental changes that we will make, such as lexical
scoping and proper structure support in an attempt to gradually move
the language towards an upwardly-compatible goal, such as Common Lisp.
I am writing this page primarily as food for thought. I feel fairly
strongly that actually doing this work would be a big waste of effort
that would inevitably become a huge time sink on the part of nearly
everyone involved in XEmacs development, and not only for the ones who
were supposed to be actually doing the engine change. I feel that
most of the desired changes that we want for the language and/or the
engine can be achieved with much less effort and time through
incremental changes to the existing code base.
First of all, in order to make a successful Lisp engine change in
XEmacs, it is vitally important that the work be done through a series
of incremental stages where at the end of each stage XEmacs can be
compiled and run, and it works. It is tempting to try to make the
change all at once, but this would be disastrous. If the resulting
product worked at all, it would inevitably contain a huge number of
subtle and extremely difficult to track down bugs, and it would be
next to impossible to determine which of the myriad changes made
introduced the bug.
Now let's look at what the possible stages of implementation could be.
An Extra C Preprocessing Stage
The first step would be to introduce another preprocessing stage
for the XEmacs C code, which is done before the C compiler itself is
invoked on the code, and before the standard C preprocessor runs.
The C preprocessor is simply not powerful enough to do many of the
things we would like to do in the C code. The existing results of
this have been a combination of a lot of hacked up and
tricky-to-maintain stuff (such as the DEFUN macro, and
the associated DEFSUBR ), as well as code constructs that
are difficult to write. (Consider for example, attempting to do
structured exception handling, such as catch/throw and unwind-protect
constructs), as well as code that is potentially or actually unsafe
(such as the uses of alloca ), which could easily cause
stack overflow with large amounts of memory allocated in this
fashion.) The problem is that the C preprocessor does not allow
macros to have the power of an actual language, such as C or Lisp.
What our own preprocessor should do is allow us to define macros,
whose definitions are simply functions written in some language which
are executed at compile time, and whose arguments are the actual
argument for the macro call, as well as an environment which should
have a data structure representation of the C code in the file and
allow this environment to be queried and modified. It can be debated
what the language should be that these extensions are written in.
Whatever the language chosen, it needs to be a very standard language
and a language whose compiler or interpreter is available on all of
the platforms that we could ever possibly consider putting XEmacs to,
which is basically to say all the platforms in existence. One obvious
choice is C, because there will obviously be a C compiler available,
because it is needed to compile XEmacs itself. Another possibility is
Perl, which is already installed on most systems, and is universally
available on all others. This language has powerful text processing
facilities which would probably make it possible to implement the
macro definitions more quickly and easily; however, this might also
encourage bad coding practices in the macros (often simple text
processing is not appropriate, and more sophisticated parsing or
recursive data structure processing needs to be done instead), and
we'd have to make sure that the nested data structure that comprises
the environment could be represented well in Perl. Elisp would not be
a good choice because it would create a bootstrapping problem. Other
possible languages, such as Python, are not appropriate, because most
programmers are unfamiliar with this language (creating a
maintainability problem) and the Python interpreter would have to be
included and compiled as part of the XEmacs compilation process
(another maintainability problem). Java is still too much in flux
to be considered at this point.
The macro facility that we will provide needs to add two features
to the language: the ability to define a macro, and the ability to
call a macro. One good way of doing this would be to make use of
special characters that have no meaning in the C language (or in C++
for that matter), and thus can never appear in a C file outside of
comments and strings. Two obvious characters are the @ sign and the $
sign. We could, for example, use @ defined to define new
macros, and the $ sign followed by the macro name to call
a macro. (Proponents of Perl will note that both of these characters
have a meaning in Perl. This should not be a problem, however,
because the way that macros are defined and called inside of another
macro should not be through the use of any special characters which
would in effect be extending the macro language, but through function
calls made in the normal way for the language.)
The program that actually implements this extra preprocessing
stage needs to know a certain amount about how to parse C code. In
particular, it needs to know how to recognize comments, strings,
character constants, and perhaps certain other kinds of C tokens, and
needs to be able to parse C code down to the statement level. (This
is to say it needs to be able to parse function definitions and to
separate out the statements, if blocks,
while blocks, etc. within these definitions. It probably
doesn't, however need to parse the contents of a C expression.) The
preprocessing program should work first by parsing the entire file
into a data structure (which may just contain expressions in the form
of literal strings rather than a data structure representing the
parsed expression). This data structure should become the environment
parameter that is passed as an argument to macros as mentioned above.
The implementation of the parsing could and probably should be done
using lex and yacc . One good idea is simply
to steal some of the lex and yacc code that
is part of GCC.
Here are some possibilities that could be implemented as part of
the preprocessing:
A proper way of doing the DEFUN macros. These
could, for example, take an argument list in the form of a Lisp
argument list (complete with keyword parameters and other complex
features) and automatically generate the appropriate subr
structure, the appropriate C function definition header, and the
appropriate call to the DEFSUBR initialization
function.
A truly safe and easy to use implementation of the
alloca function. This could allocate the memory in any
fashion it chooses (calling malloc using a large global
array, or a series of such arrays, etc.) an insert in the
appropriate places to automatically free up this memory. (Appropriate
places here would be at the end of the function and before any return
statements. Non-local exits can be handled in the function that
actually implements the non-local exit.)
If we allow for the possibility of having an arbitrary Lisp
engine, we can't necessarily assume that we can call Lisp primitives
implemented in C from other C functions by simply making a function
all. Perhaps something special needs to happen when this is done.
This could be handled fairly easily by having our new and improved
DEFUN macro define a new macro for use when calling a
primitive.
Make the Existing Lisp Engine be Self-contained.
The goal of this stage is to gradually build up a self-contained
Lisp engine out of the existing XEmacs core, which has no dependencies
on any of the code elsewhere in the XEmacs core, and has a
well-defined and black box-style interface. (This is to say that the
rest of the C code should not be able to access the
implementation of the Lisp engine, and should make as few assumptions
as possible about how this implementation works). The Lisp engine
could, and probably should, be built up as a separate library which
can be compiled on its own without any of the rest of the XEmacs
C code, and can be tested in this configuration as
well.
The creation of this engine library should be done as a series of
subsets, each of which moves more code out of the XEmacs core and
into the engine library, and XEmacs should be compilable and runnable
between each sub-step. One possible series of sub-steps would be to
first create an engine that does only object allocation and garbage
collection, then as a second sub-step, move in the code that handles
symbols, symbol values, and simple binding, and then finally move in
the code that handles control structures, function calling,
byte-code execution, exception handling, etc. (It might
well be possible to further separate this last sub-step).
Removal of Assumptions About the Lisp Engine Implementation
Currently, the XEmacs C code makes all sorts of assumptions about
the implementation of the Lisp engine, particularly in the areas of
object allocation, object representation, and garbage collection. A
different Lisp engine may well have different ways of doing these
implementations, and thus the XEmacs C code must be rid of any
assumptions about these implementations. This is a tough and tedious
job, but it needs to be done. Here are some examples:
GCPRO must go. The GCPRO mechanism
is tedious, error-prone, unmaintainable, and fundamentally unsafe. As
anyone who has worked on the C Core of XEmacs knows, figuring out
where to insert the GCPRO calls is an exercise in black
magic, and debugging crashes as a result of incorrect
GCPROing is an absolute nightmare. Furthermore, the
entire mechanism is fundamentally unsafe. Even if we were to use the
extra preprocessing stage detailed above to automatically generate
GCPRO and UNGCPRO calls for all Lisp object
variables occurring anywhere in the C code, there are still places
where we could be bitten. Consider, for example, code which calls
cons and where the two arguments to this functions are
both calls to the append function. Now the
append function generates new Lisp objects, and it also
calls QUIT , which could potentially execute arbitrary
Lisp code and cause a garbage collection before returning control to
the append function. Now in order to generate the
arguments to the cons function, the append
function is called twice in a row. When the first append
call returns, new Lisp data has been created, but has no
GCPRO pointers to it. If the second append
call causes a garbage collection, the Lisp data from the first
append call will be collected and recycled, which is
likely to lead to obscure and impossible-to-debug crashes. The only
way around this would be to rewrite all function calls whose
parameters are Lisp objects in terms of temporary variables, so that
no such function calls ever contain other function calls as arguments.
This would not only be annoying to implement, even in a smart
preprocessor, but would make the C code become incredibly slow
because of all the constant updating of the GCPRO
lists.
The only proper solution here is to completely do away with the
GCPRO mechanism and simply do conservative garbage
collection over the C stack. There are already portable
implementations of conservative pointer marking over the C stack, and
these could easily be adapted for use in the Elisp garbage collector.
If, as outlined above, we use an extra preprocessing stage to create
a new version of alloca that allocates its memory
elsewhere than actually on the C stack, and we ensure that we don't
declare any large arrays as local variables, but instead use
alloca , then we can be guaranteed that the C stack is
small and thus that the conservative pointer marking stage will be
fast and not very likely to find false matches.
Removing the GCPRO declarations as just outlined
would also remove the assumption currently made that garbage
collection can occur only in certain places in the C code, rather than
in any arbitrary spot. (For example, any time an allocation of Lisp
data happens). In order to make things really safe, however, we also
have to remove another assumption as detailed in the following
item.
Lisp objects might be relocatable. Currently, the C code
assumes that Lisp objects other than string data are not relocatable
and therefore it's safe to pass around and hold onto the actual
pointers for the C structures that implement the Lisp objects.
Current code, for example, assumes that a Lisp_Object of
type buffer and a C pointer to a struct buffer mean
basically the same thing, and indiscriminately passes the two kinds of
buffer pointers around. With relocatable Lisp objects, the pointers
to the C structures might change at any time. (Remember, we are now
assuming that a garbage collection can happen at basically any point).
All of the C code needs to be changed so that Lisp objects are always
passed around using a Lisp object type, and the underlying pointers
are only retrieved at the time when a particular data element out of
the structure is needed. (As an aside, here's another reason why Lisp
objects, instead of pointers, should always be passed around. If
pointers are passed around, it's conceivable that at the time a
garbage collection occurs, the only reference to a Lisp object (for
example, a deleted buffer) would be in the form of a C pointer rather
than a Lisp object. In such a case, the conservative pointer marking
mechanism might not notice the reference, especially if, in an attempt
to eliminate false matches and make the code generally more efficient,
it will be written so that it will look for actual Lisp object
references.)
I would go a step farther and completely eliminate the macros
that convert a Lisp object reference into a C pointer. This way the
only way to access an element out of a Lisp object would be to use the
macro for that element, which in one atomic operation de-references
the Lisp object reference and retrieves the value contained in the
element. We probably do need the ability to retrieve actual C
pointers, though. For example, in the case where an array is stored
in a Lisp object, or simply for efficiency purposes where we might
want some code to retrieve the C pointer for a Lisp object, and work
on that directly to avoid a whole bunch of extra indirections. I
think the way to do this would be through the use of a special locking
construct implemented as part of the extra preprocessor stage
mentioned above. This would essentially be what you might call a
lock block, just like a while block. You'd
write the word lock followed by a parenthesized
expression that retrieves the C pointer and stores it into a variable
that is scoped only within the lock block and followed in turn by some
code in braces, which is the actual code associated with the lock
block, and which can make use of this pointer. While the code inside
the lock block is executing, that particular pointer and the object
pointed to by it is guaranteed not to be relocated.
If all the XEmacs C code were converted according to these
rules, there would be no restrictions on the sorts of implementations
that can be used for the garbage collector. It would be possible, for
example, to have an incremental asynchronous relocating garbage
collector that operated continuously in another thread while XEmacs
was running.
The C implementation of Lisp objects might not, and probably
should not, be visible to the rest of the XEmacs C code. It should
theoretically be possible, for example, to implement Lisp objects
entirely in terms of association lists, rather than using C structures
in the standard way. (This may be an extreme example, but it's good
to keep in mind an example such as this when cleaning up the XEmacs C
code). The changes mentioned in the previous item would go a long way
towards removing this assumption. The only places where this
assumption might still be made would be inside of the lock blocks
where an actual pointer is retrieved. (Also, of course, we'd have to
change the way that Lisp objects are defined in C so that this is done
with some function calls and new and improved macros rather than by
having the XEmacs C code actually define the structures. This sort of
thing would probably have to be done in any case once the allocation
mechanism is moved into a separate library.) With some thought it
should be possible to define the lock block interface in such a way as
to remove any assumptions about the implementation of Lisp
objects.
C code may not be able to call Lisp primitives that are defined
in C simply by making standard C function calls. There might need to
be some wrapper around all such calls. This could be achieved cleanly
through the extra preprocessing step mentioned above, in line with
the example described there.
Actually Replacing the Engine.
Once we've done all of the work mentioned in the previous steps
(and admittedly, this is quite a lot of work), we should have an
XEmacs that still uses what is essentially the old and previously
existing Lisp engine, but which is ready to have its Lisp engine
replaced. The replacement might proceed as follows:
Identify any further changes that need to be made to the engine
interface that we have defined as a result of the previous steps so
that features and idiosyncrasies of various Lisp engines that we
examine could be properly supported.
Pick a Lisp engine and write an interface layer that sits on
top of this Lisp engine and makes it adhere to what I'll now call the
XEmacs Lisp engine interface.
Strongly consider creating, if we haven't already done so, a
test suite that can test the XEmacs Lisp engine interface when used
with a stand-alone Lisp engine.
Test the hell out of the Lisp engine that we've chosen when
combined with its XEmacs Lisp engine interface layer as a stand-alone
program.
Now finally attach this stand-alone program to XEmacs itself.
Debug and fix any further problems that ensue (and there inevitably
will be such problems), updating the test suite as we go along so that
if it were run again on the old and buggy interfaced Lisp engine, it
would note the bug.
Ben Wing
|