🌱Ian's Digital Garden

💾
Midnight System


From Cons to Cursor

A magical property of computers is the ability to "change the airplane engines mid-flight." You can start with Linux, GCC, Git and Vim and-- given enough time-- rewrite them into whatever software you'd like.

Some people are interested in the smallest systems that provide this ability. For instance Hundred Rabbits has their 💾uxn virtual computer. These minimal systems are less useful than Linux, but they're easier to understand. That makes them handy for studying this self-modifying behavior of computers.

Midnight's also a minimal self-modifying computing system. However it comes at the problem from a Lisp perspective insted of an assembly one. It's designed to answer the question: if we start from Lisp primitives like cons, car, and cdr, what's the smallest amount of stuff we need to add to get a self-modifying system without code golfing? Are there any special characteristics of lisp that make this easier?

Result

A miniature, Scheme-like language (Midnight Lisp) and a computing environment based on it (the Midnight System).

Live: midnightsystem.com

Code: github.com/seagreen/midnight

Steps

1. "Machine" Language

The primitives are inspired by the original Lisp paper, 📜Recursive Functions of Symbolic Expressions and Their Computation by Machine.

lambda, let, quote, eval, if, car, cdr, cons, pair?, list-empty?, symbol? symbol-eq?, codepoints->symbol, symbol->codepoints, int?, +, -, *, /, %, <, =, >

See more: 💾Midnight Lisp.

2. Macro System

Midnight is a nice base language, but we want a higher-level language to write the rest of the system in.

In a traditional system we'd write a compiler and have separate source code files for both languages.

In Midnight, taking advantage of quote, we write a macro system. Using it we incrementally build the higher level langauge. This all happens in a single expression:

(let
  ((midnight-plus-macros
    '(
; definitions here must start with `define` or `define-macro`
))
  (macroexpand ...)) ; implement the macro system

(eval (macroexpand (midnight-plus-macros))))

Using define-macro we write the following:

  • Short circuiting and and or
  • cond
  • Pattern matching with case
  • quasiquote
  • define-struct

3. Stdlib

List functions, association list based dictionaries, etc.

4. Input, Output, Store, and Ephem

These aren't part of the source code, but rather properties of the system itself.

Input: keypresses.

Output: a 80x30 fixed text display and a cursor.

Store ("hard drive"): an S-expression. It's passed into each step of the program along with a keypress and an Ephem. If a running program had a type signature it would be:

step :: Store -> Ephem -> Keypress -> (Store, Ephem)

The store must be an association list with a display key. The system reads off the display key after each input to know what to display.

Ephem ("RAM"-- except tree-shaped so TM?): A Midnight expression. Unlike the Store this can contain closures.

5. Text Editor

Back to the source code.

The two simplest programs I could think of to give self-modifying behavior are a text editor and a REPL. I'm still thinking about which is the best choice for this project, but it's a text editor for now.

6. Self-Modifying Machinery

To get self-modifying behavior the source code stores the actual action to be taken on each keypress as a closure in the Ephem. The source feeds each input into the Ephem closure instead of processing it itself. Thus the system can change its own behavior by replacing the closure stored there.

Currently the user requests such a reload with a ctrl-ENTER keypress. This triggers a parse of the editor's current contents, using a parser for Midnight written in Midnight. This is then eval'd. The result replaces the old Ephem closure.

Reflections

How'd we do? Our original question was if we start from Lisp primitives like cons, car, and cdr, what's the smallest amount of stuff we need to add to get a self-modifying system without code golfing? Are there any special characteristics of lisp that make this easier?

The answer to the first part is about ~3,000 lines of code when done this way.

And does lisp make this easier? Yes!

  • quote means we can store the logic of the whole program, including multiple "languages", in a single file, in fact a single expression. You wouldn't want to do this for a big system but it works here.
  • Using a tiny lisp for the base language gives a nice balance of precision, ease of implementation, and human-readability.
  • The minimal syntax makes parse easy to write.

Bonus: Copy-Paste Your Computer

Midnight supports system images (Wikipedia link).

Steps:

  1. From within a running system, click the "Store" button at the top alongside "Live", "Display", etc.
  2. Copy the text there
  3. Paste it into the "Store" button text box in another tab
  4. Press ctrl-ENTER

The system will take up from exactly where it left off.

How Paste Works

The difficulty in starting from a pasted system is rebuilding the Ephem, since it can contain closures and thus can't be guaranteed to be serializable.

To get around this applications are required to keep another special key on the Store in addition to display called main-sexp. main-sexp must contain an S-expression that, when evaluated, produces a function that can rebuild the appropriate Ephem.

Related Work

📎list of miniature computing systems

📎list of minimalist lisp-family programming languages

Backlinks