Past Work

Edwin48

Porting the Scheme editor used in MIT's SICP course. In the summer of 2008, I joined the work to bring Edwin to Scheme48 and Scsh.

Explore the source on GitHub
MIT Scheme's original Edwin editor showing its Scheme interaction buffer in a Mac window.
MIT Scheme's Edwin editor.

A lot of old-time Lisp programmers and hackers speak longingly of the days of Lisp machines. Outside an emulator, Emacs is probably the closest you can get to that experience today.

In Emacs and Edwin, you could inspect the tools you were using and change their behavior while they were running. The editor was a place to program your working environment from within.

In MIT's Edwin, the Scheme interpreter and debugger were part of that everyday environment. An error could take you into a stopped computation, where you could inspect its bindings and evaluate another expression to understand what had happened. The original Edwin manual describes that interaction in detail.

That is the sense in which I think of these editors as small Lisp machines: a way of working inside a running language. Edwin48 was our attempt to bring Edwin to Scheme48 and Scsh.

The idea reaches back to MIT's editing culture. The first Emacs grew out of TECO on a PDP-10 in 1976. Its users could add commands while editing. Lisp-based successors, including EINE and ZWEI on MIT Lisp machines and Bernard Greenberg's Multics Emacs, carried that approach into a language better suited to extension. These were different implementations of a shared idea. Richard Stallman's 1981 Emacs paper tells that history.

Scheme grew from Guy Steele and Gerald Sussman's work in the 1970s. MIT's implementation became a teaching and programming environment, with Edwin as its Emacs-style editor. Scheme was the language of both the programs you edited and the editor itself.

By 1993, Matthew Birkholz was implementing Emacs Lisp inside Edwin so existing extensions could run there. His target was GNUS, the news reader. Choosing Scheme already meant confronting Emacs's valuable body of existing software.

Edwin was also used in MIT's famous 6.001 course, Structure and Interpretation of Computer Programs. The spring 2005 course materials introduced students to Edwin and MIT Scheme together. This was the editor Duncan Mak imported in January 2008, and the codebase I joined him in porting that summer.

One attraction of Scheme was lexical scope. A name refers to a binding determined by the surrounding program text. A closure keeps access to those bindings after the procedure that created it returns. That matters when an editor passes commands and callbacks between components: the code calling a procedure should not accidentally change what its local names mean.

In 2008, ordinary GNU Emacs Lisp bindings were dynamically scoped. A free name could pick up a temporary binding further up the call chain. The problem was longstanding. The history of Emacs Lisp dates the lexical-let workaround to 1993. Native lexical-binding support arrived in Emacs 24.1 in June 2012, so this is a distinction about the time we were working.

Scheme's let family made local relationships explicit: let introduced bindings, let* made them sequentially, and letrec allowed mutually recursive procedures. Temporary dynamic context still had a job. An operation could establish a setting for everything it called, then restore the previous setting on exit.

Duncan's February 2008 adapter tried to preserve MIT Scheme's fluid-let through Scheme48's let-fluid. The spellings belong to those implementations, not standard Scheme. The Scheme48 fluid API used explicit cells whose values followed the current dynamic context, including continuation exits and re-entry.

(define-syntax fluid-let
  (syntax-rules ()
    ((fluid-let ((variable init))
       expression ...)
     (let-fluid variable init
       (lambda () expression ...)))))
Duncan Mak · February 25, 2008

An adapter for a single fluid-let binding.

Read the fluid-binding change

A variable binding connects a name to a location holding a value. An environment supplies those bindings, with a parent environment to search when a name is not found locally. Two environments can bind the same name differently: width might mean 80 in one and 132 in another.

MIT Scheme made environments first-class objects. You could keep one in a variable, pass it to a procedure, inspect its bindings, or give it to the evaluator. Evaluating the same expression in two different environments could produce different results. This went beyond changing a setting: you could choose the context in which Scheme code acquired its meaning.

For a live editor, that was powerful. Edwin buffers could select an environment for evaluating Scheme, and completion could use the names bound there. The MIT Scheme manual even notes that the same prefix could complete differently in different buffers.

The flexibility also made the codebase difficult for me to reason about. Reading a name was not always enough; I had to establish which environment supplied its binding and what had been loaded or defined there. Lexical scope still applied: a procedure retained its lexical bindings. Switching buffers did not silently give an existing procedure a different scope.

Two illustrative MIT Scheme environments bind width to different values. Evaluating width in environment A returns 80; evaluating the same name in environment B returns 132. Each evaluation uses its selected environment, not a change to lexical scope.

The Scheme Underground had a practical ambition: use this small language to build the tools people needed every day. Olin Shivers's 1995 Web-system documentation describes a server and networking tools built with Scheme48 and Scsh. Extensions ran in the same language as the built-in services.

MIT's Scheme project page later described an environment for Unix, the Web, and wearable computers. Our editor work belonged to that practical Scheme tradition. Scsh supplied operating-system access; an extensible editor would give us another tool to work in. Olin Shivers's 1994 paper, A Scheme Shell describes Scsh's design: Unix pipelines and redirections expressed through Scheme macros, alongside the rest of the language.

The module system was what drew us to Scheme48. It felt more modern than MIT Scheme's approach, with explicit descriptions of what each component imported and exported.

In the Scheme48 module system, a package was a module's internal environment. A structure exposed selected bindings, and an interface specified the exported names. MIT Scheme had packages too; Scheme48 gave us a clearer place to follow dependencies while retaining an interactive development environment.

There is good hacker history behind that design. Jonathan Rees recalls getting the first Scheme48 working with Richard Kelsey within 48 hours in 1986. The module system later grew out of sharing code between a mobile robot, its remote debugger, and a fuller development system. His account of Scheme48's beginnings makes the practical origins clear.

The manual also explicitly credits Symbolics for separating command-processor commands from Scheme expressions. Debugging commands could remain available without depending on the names visible in the current package. The Lisp-machine connection was more than a visual reference.

Making the module boundaries work was part of our port. Duncan separated the basic-command module to avoid a circular dependency. I worked on package definitions alongside windows, terminal input, and search. Sharing the Scheme language did not make its implementations interchangeable.

My first recorded change, in June, adapted the console code from MIT Scheme's channels to Scsh's input and output ports. I then worked on optional arguments and the editor's window objects. In July, I ported character searching to standard Scheme macros and SRFI-13 string operations, including the helpers for case-insensitive searches.

The search port is a small example of what I liked about Scheme. A procedure, make-find-next, accepted a search function and returned a closure that retained it. Different searches could share the same handling of the editor's gap buffer. A short syntax-rules macro supplied the definition syntax. Hygienic macros kept introduced helper names tied to the intended bindings.

Duncan worked through the terminal and window system at the same time. His changes handled raw terminal input and introduced a new representation for keystrokes and their modifiers. The editor needed to agree on what a key meant before it could decide what to do with it.

(define-syntax define-next-char-search
  (syntax-rules ()
    ((_ name find-next)
     (define name
       (make-find-next find-next)))))

(define-next-char-search
  group-find-next-char string-index)
Jeff Dlouhy · July 11, 2008

The macro gives each search function the same definition syntax.

Read the search port

The editor's window code also needed classes and methods. Soosy supplied a small object system for Scheme48. Duncan had built its class and object records; in June 2008 I added macros for defining and calling methods, including explicit calls to a superclass.

An instance held its own fields and a reference to its class. The class's method table supplied its behavior. In the point example from my tests, a colored point inherited x and y and added a color field, c. It also overrode the point's :say method. An ordinary send selected that override.

The usual==> form explicitly selected the method from the receiver's immediate superclass instead. Ordinary sends did not search up that chain: inherited methods were copied into the class's method table. The diagram uses the two calls from the test file to show the difference.

Soosy instance cpt has fields x, y, and c and references class color-point, whose superclass is point. Both classes have a say method; color-point overrides it. The call (==> cpt :say "override") prints word: override. The explicit superclass call (usual==> cpt :say "usual") prints usual. Inherited methods are copied into class method tables, rather than searched through a superclass chain on each ordinary send.

An Emacs-style editor also has key bindings: associations between keystrokes and commands. These are distinct from Scheme's variable bindings. An editing mode chooses editing behavior and command tables; an evaluation environment determines what Scheme names mean. A buffer can have both kinds of context.

I began porting Edwin's command tables in August 2008. A table maps a key to either a command or another table. Control-X selects a prefix table, then Control-F selects find-file within it. Control-H followed by k reaches describe-key through a different table.

I continued that work in spring 2009 with a hash-table-based implementation, a design note, and tests for bindings, prefixes, and lookup precedence. A mode could supply an ordered list of tables, with the first matching binding winning. Changing that context let the same keys do different work in different modes.

The plate below follows the table layout in my notes. It shows prefix lookup within one table tree; the ordering of a mode's tables is a separate step.

An Edwin48 root command table maps Control-A to move-beginning-of-line and Control-X and Control-H to prefix tables. The Control-X table maps Control-F to find-file, Control-D to list-directory, and Control-S to save-buffer. The Control-H table maps k to describe-key and a to apropos-command. Control-X Control-F selects find-file; Control-H k selects describe-key.

Pantene brought another Lisp idea into the project: conditions and restarts. The code that found a problem could describe it and provide recovery operations. A handler elsewhere could decide which recovery to use. The program did not have to discard the failing context before asking what to do next.

Duncan began Pantene, and Roderic Morris implemented much of its conditions and restarts. In May 2009 Roderic integrated it into Edwin48. His earlier condition-fix commit also thanks me for help.

One test gives the idea a concrete shape. A worker processes a list of pairs. When it encounters a malformed entry, an outer handler chooses the use-value restart and supplies -1 for that entry. Processing then continues to the next pair. Scheme continuations provide the return point; dynamically bound parameters keep track of the available handlers and restarts.

Edwin's inherited command reader likewise had an ABORT-EDITOR-COMMAND restart for returning to the command loop. These were the recovery mechanisms we were working to connect. The port did not complete Pantene's interactive restart chooser and debugger.

(lambda (condition)
  (use-value -1 condition))
Pantene test · May 20, 2009

The handler supplies -1 for the malformed entry so processing can continue.

Read the condition and restart test

The work crossed repository boundaries. Scsh supplied shell and operating-system interfaces; Soosy supplied objects; Terminfo described the capabilities of different terminals. In October 2010 Roderic fixed Terminfo's database lookup on Ubuntu and corrected how it read binary data.

Our June 2008 README named NuScsh as the development platform and recorded builds on Ubuntu Hardy and Mac OS X Leopard. Roderic carried the larger Scsh runtime port. My own recorded change to the shell was a small Mac build fix in October 2010: removing the Autoconf 2.67 requirement that blocked my setup, which had 2.61.

Scheme's control facilities also had ordinary housekeeping to do. Duncan's October 2008 terminal demo used dynamic-wind to change terminal mode for an operation and restore the saved settings afterward. Its entry and exit actions also accounted for movement through continuations. Leaving the terminal in the wrong mode would be a rather immediate reminder that language design has practical consequences.

In February 2014, I also worked on a Scsh homepage prototype using Middleman. The website repository preserves four commits covering setup, design, and documentation. It still has placeholder links; the surviving work does not establish a launch.

(dynamic-wind
  (lambda ()
    (set-input-terminal-mode mode port))
  thunk
  (lambda ()
    (set-input-terminal-mode orig port)))
Duncan Mak · October 2, 2008

Set the terminal mode for an operation, then restore the previous settings.

Read the terminal-mode code

Edwin48 remained an experimental port. In April 2013 I committed Cosmacs, a small test application for connecting terminal input to Edwin commands. The surviving work includes that experiment, command-table tests, and substantial ported code, but it was not a finished replacement for Emacs.

The repository preserves the implementation and its history. Its remaining task list records work still needed on search, events, and terminal sizing.

← All past work