
Gábor Melis — Adaptive Hashing
@2025-05-02 00:00 · 2 days agoAt the 2024 ELS, I gave a talk on adaptive hashing, which focusses on making general purpose hash tables faster and more robust at the same time.
Theory vs Practice
Hash table theory most concerns itself with the asymptotic worst-case cost with a hash function chosen randomly from a family of hash functions. Although these results are very relevant in practice,
those pesky constant factors, that the big-O cost ignores, do matter, and
we don't pick hash functions randomly but fix the hash function for the lifetime of the hash table.
There are Perfect Hashing algorithms, that choose an optimal hash function for a given set of keys. The drawback is that they either require the set of keys to be fixed or they are too slow to be used as general purpose hash tables.
Still, the idea that we can do better by adapting the hash function to the actual keys is key. Can we do that online, that is, while the hash table is being used? Potential performance gains come from improving the constant factors mentioned above by
having fewer collisions, and
being more cache-friendly.
The images above plot regret (the expected number of comparisons of
per lookup minus the minimum achievable) and the measured run-time
of PUT operations vs the number of keys in the hash table with a
particular key distribution. Green is Murmur (a robust hash
function), Blue is SBCL's expedient EQ
hash. The
wiggling of the graphs is due to the resizing of the hash table as
keys are added to it.
Note how SBCL's regret starts out much lower and becomes much higher than that of Murmur, but if anything, it advantage grows.
Implementation
The general idea is sound, but turning it into real performance gains is hard due to the cost of choosing a hash function and switching to it. First, we have to make some assumption about the distribution of keys. In fact, default hash functions in language runtimes often make such assumptions to make the common cases faster, usually at the cost of weakened worst-case guarantees.
The rest of this post is about how SBCL's built-in hash tables, which had been reasonably fast, were modified. The core switching mechanism looks at
the length of the collision chain on PUT operations,
the collision count on rehash (when the hash table is grown), and
the size of the hash table.
Adapting EQ
hash tables
Init to to constant hash function. This a fancy way of saying that we do linear search in a vector internally. This is an
EQ
hash table, so key comparison is as single assembly instruction.When the hash table is grown to more than 32 keys and it must be rehashed anyway, we switch to a hash function that does a single right shift with the number of bits to shift determined from the longest common run of low-bits in the keys.
If too many collisions, we switch to the previous default SBCL
EQ
-hash function that has been tuned for a long time.If too many collisions, we switch to Murmur, a general purpose hash. We could also go all the way to cryptographic hashes.
In step 2, the hash function with the single shift fits the memory allocator's behaviour nicely: it is a perfect hash for keys forming arithmetic sequences, which is often approximately true for objects of the same type allocated in a loop.
In this figure, the red line is the adaptive hash.
Adapting EQUAL
hash tables
For composite keys, running the hash function is the main cost. Adaptive hashing does the following.
For string keys, hash only the first and last 2 characters.
For list keys, only hash the first 4 elements.
If too many collisions, double the limit.
So, SBCL hash tables have been adaptive for almost a year now,
gaining some speed in common cases, and robustness in others.
The full paper is here.

Joe Marshall — It Still Sucks
@2025-05-01 12:29 · 2 days agoDon’t get me wrong. I”m not saying that the alternatives are any better or even any different.
Unix has been around more than forty years and it is still susceptible to I/O deadlock when you try to run a subprocess and stream input to it and output from it. The processes run just fine for a while, then they hang indefinitely waiting for input and output from some buffer to synchronize.
I’m trying to store data in a database. There aren't any good database bindings I could find, so I wrote a small program that reads a record from stdin and writes it to the database. I launch this program from Common Lisp and write records to the input of the program. It works for about twenty records and then hangs. I've tried to be careful to flush and drain all streams from both ends, to no avail.
I have a workaround: start the program, write one record, and quit the program. This doesn’t hang and reliably writes a record to the database, but it isn’t fast and it is constantly initializing and creating a database connection and tearing it back down for each record.
You'd think that subprocesses communicating via stream of characters would be simple.
Neil Munro — Ningle Tutorial 6: Database Connections
@2025-04-30 21:30 · 3 days agoContents
- Part 1 (Hello World)
- Part 2 (Basic Templates)
- Part 3 (Introduction to middleware and Static File management)
- Part 4 (Forms)
- Part 5 (Environmental Variables)
- Part 6 (Database Connections)
Introduction
Welcome back, in this tutorial we will begin looking at how to work with SQL databases, specifically SQLite3, MySQL, and PostgreSQL. We will be using the mito ORM to create user models and save them to the database using the form
we created previously. Mito
itself is a basic ORM and includes several mixins to provide additional functionality, we will use one called mito-auth to provide password hashing and salting.
It is important to know that mito
is based on top of a SQL library known as SXQL, we will occasionally use SXQL
to write queries with mito
, while it's not always required to use SXQL, there are times where it will make life easier. To achieve this, I elected to :use
SXQL in my package definition.
(defpackage ningle-tutorial-project
(:use :cl :sxql)
Part of working with databases using an ORM is creating the initial database/tables and managing changes over time, called migrations
, mito
appears to have a migrations system, although I was unable to get it working, but I developed a means by which to apply migrations, so perhaps in a future tutorial the subject can be revisited. As such, in addition to seeing how to connect to the respective SQL databases, we will write implementation specific migration functions.
We will follow the example of setting up a secure user registration system across all three SQL implementations. One thing to bear in mind is that it is beyond the scope of this tutorial to instruct how to setup MySQL or PostgreSQL, I would recommend learning how to set them up using docker. All that having been said, let's have a look at the different databases and how to connect to them!
Please bear in mind that when working with SQLite remember to add .db
to your .gitignore
as you most certainly don't want to accidentally commit a database into git! SQLite, being a file based database (unlike MySQL and PostgreSQL) will create a file that represents your database so this step only applies to SQLite.
Installing Packages
To begin with we will need to ensure we have installed and added the packages we need to our project asd file, there are three that we will be installing:
As normal you will need to add them in the :depends-on
section. Please note however that there is an issue in mito-auth
that prevents it from working in MySQL, I have submitted a fix but it has not been merged yet, so for now you can use my branch, if you do, please ensure you check it out via git into your quicklisp/local-projects
directory.
:depends-on (:clack
:cl-dotenv
:djula
:cl-forms
:cl-forms.djula
:cl-forms.ningle
:ingle
:mito
:mito-auth
:ningle)
Mito is a package for managing models/tables in our application, mito-auth is a mixin
that enables models to have a secure password field, not all models will need this, but our user model will! ingle
is a small library that includes some very useful utilities, one of which is a redirect
function which will be very useful indeed!
Now that that is done, we must set up the middleware, you might remember from Part 3 that middleware is placed in the lack.builder:builder
function call in our start
function.
SQL Middleware
Mito provides middleware to establish and manage database connections for SQLite3
, MySQL
, and PostgreSQL
, when you build your solution you will need to pick a database implementation, for production systems I suggest PostgreSQL
, but if you are just starting out, you can use SQLite
.
SQLite3
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder
:session
`(:mito
(:sqlite3
:database-name ,(uiop:getenv "SQLITE_DB_NAME")))
(:static
:root (asdf:system-relative-pathname :ningle-tutorial-project "src/static/")
:path "/public/")
*app*)
:server server
:address address
:port port))
MySQL
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder
:session
`(:mito
(:mysql
:database-name ,(uiop:native-namestring (uiop:parse-unix-namestring (uiop:getenv "MYSQL_DB_NAME")))
:username ,(uiop:getenv "MYSQL_USER")
:password ,(uiop:getenv "MYSQL_PASSWORD")
:host ,(uiop:getenv "MYSQL_ADDRESS")
:port ,(parse-integer (uiop:getenv "MYSQL_PORT"))))
(:static
:root (asdf:system-relative-pathname :ningle-tutorial-project "src/static/")
:path "/public/")
*app*)
:server server
:address address
:port port))
PostgreSQL
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder
:session
`(:mito
(:postgres
:database-name ,(uiop:native-namestring (uiop:parse-unix-namestring (uiop:getenv "POSTGRES_DB_NAME")))
:username ,(uiop:getenv "POSTGRES_USER")
:password ,(uiop:getenv "POSTGRES_PASSWORD")
:host ,(uiop:getenv "POSTGRES_ADDRESS")
:port ,(parse-integer (uiop:getenv "POSTGRES_PORT"))))
(:static
:root (asdf:system-relative-pathname :ningle-tutorial-project "src/static/")
:path "/public/")
*app*)
:server server
:address address
:port port))
Testing The Connection
Before we go further with building models and migration functions, we should test that the connections work and the most basic of SQL statements. We will be working on our register controller, so that seems like as good a place as any to place a simple check.
(setf (ningle:route *app* "/register" :method '(:GET :POST))
(lambda (params)
(let ((query (mito:retrieve-by-sql "SELECT 2 + 3 AS result")))
(format t "Test: ~A~%" query))
(let ((form (cl-forms:find-form 'register)))
...
Here, in the controller we have added a small (temporary) check to ensure that the database connections are set up correctly, when you run the application and perform a GET
request on this route, you should see the output printed in the console for:
Test: ((RESULT 5))
It might look a little odd, but rest assured that this is proof that everything is right and the connection works! We will be removing this later as it serves just as a small check. So with that done, we can begin to look into writing our first model, our user model.
Creating Models
Models are a way to represent both a generic object, and any specific object of that type in a relational database system. For example you might have a Book model, that represents a book table, however a book is just a way to classify something any doesn't tell you anything specific about any individual book. So here we will create a User model, that refers to all users, but each instance of a User will contain the specific information about any given user.
We will create a new file called models.lisp
:
(defpackage ningle-tutorial-project/models
(:use :cl :mito)
(:export #:user))
(in-package ningle-tutorial-project/models)
(deftable user (mito-auth:has-secure-password)
((email :col-type (:varchar 255) :initarg :email :accessor email)
(username :col-type (:varchar 255) :initarg :username :accessor username))
(:unique-keys email username))
Now, mito
provides a deftable
macro
that hides some of the complexities, there is a way to use a regular class and change the metaclass
, but it's much less typing and makes the code look nicer to use the deftable
syntax. It is important to note however that we use the mixin
from mito-auth
called has-secure-password
. Obviously this mixin wouldn't be needed in all of our models, but because we are creating a user that will log into our system, we need to ensure we are handling passwords securely.
Writing Migrations
Now that we have this we need to write the migration code I mentioned earlier, databases (and their models) change over time as application requirements change, as columns get added, removed, changed, etc it can be tricky to get right and you certainly would prefer to have these done automatically, a stray SQL query in the wrong connected database can do incredible damage (trust me, I know!), so migrations allow us to track these changes and have the database system manage them for us.
This code will set up connections to the implementation we want to use and delegate migrations to mito, so pick your implementation and place it in migrations.lisp
.
SQLite3
(defpackage ningle-tutorial-project/migrations
(:use :cl :mito)
(:export #:migrate))
(in-package :ningle-tutorial-project/migrations)
(defun migrate ()
"Explicitly apply migrations when called."
(dotenv:load-env (asdf:system-relative-pathname :ningle-tutorial-project ".env"))
(format t "Applying migrations...~%")
(mito:connect-toplevel
:sqlite3
:database-name (uiop:native-namestring (uiop:parse-unix-namestring (uiop:getenv "SQLITE_DB_NAME"))))
(mito:ensure-table-exists 'ningle-tutorial-project/models:user)
(mito:migrate-table 'ningle-tutorial-project/models:user)
(mito:disconnect-toplevel)
(format t "Migrations complete.~%"))
MySql
(defpackage ningle-tutorial-project/migrations
(:use :cl :mito)
(:export #:migrate))
(in-package :ningle-tutorial-project/migrations)
(defun migrate ()
"Explicitly apply migrations when called."
(dotenv:load-env (asdf:system-relative-pathname :ningle-tutorial-project ".env"))
(format t "Applying migrations...~%")
(mito:connect-toplevel
:mysql
:database-name (uiop:native-namestring (uiop:parse-unix-namestring (uiop:getenv "MYSQL_DB_NAME")))
:username (uiop:getenv "MYSQL_USER")
:password (uiop:getenv "MYSQL_PASSWORD")
:host (uiop:getenv "MYSQL_ADDRESS")
:port (parse-integer (uiop:getenv "MYSQL_PORT")))
(mito:ensure-table-exists 'ningle-tutorial-project/models:user)
(mito:migrate-table 'ningle-tutorial-project/models:user)
(mito:disconnect-toplevel)
(format t "Migrations complete.~%"))
PostgreSQL
(defpackage ningle-tutorial-project/migrations
(:use :cl :mito)
(:export #:migrate))
(in-package :ningle-tutorial-project/migrations)
(defun migrate ()
"Explicitly apply migrations when called."
(dotenv:load-env (asdf:system-relative-pathname :ningle-tutorial-project ".env"))
(format t "Applying migrations...~%")
(mito:connect-toplevel
:postgres
:database-name (uiop:getenv "POSTGRES_DB_NAME")
:host (uiop:getenv "POSTGRES_ADDRESS")
:port (parse-integer (uiop:getenv "POSTGRES_PORT"))
:username (uiop:getenv "POSTGRES_USER")
:password (uiop:getenv "POSTGRES_PASSWORD"))
(mito:ensure-table-exists 'ningle-tutorial-project/models:user)
(mito:migrate-table 'ningle-tutorial-project/models:user)
(mito:disconnect-toplevel)
(format t "Migrations complete.~%"))
It will be necessary to add these two files into the :components
section of your project asd file.
:components ((:module "src"
:components
((:file "models")
(:file "migrations")
(:file "forms")
(:file "main"))))
Just remember if you are using MySQL or PostgreSQL, you will need to ensure that the database you want to connect to exists (in our case ntp), and that your connecting user has the correct permissions!
Running Migrations
Now that everything is set up, we will need to perform our initial migrations:
(ningle-tutorial-project/migrations:migrate)
If this has worked, you will see a lot of output SQL statements, it's quite verbose, however this only means that it is working and we can move onto actually creating and saving models.
Removing Connection Check
Now that we have migrations and models working we should remember to remove this verification code that we wrote earlier.
(let ((query (mito:retrieve-by-sql "SELECT 2 + 3 AS result")))
(format t "Test: ~A~%" query))
Registering & Querying Users
What we are going to do now is use the user register form and connect it to our database, because we are registering users we will have to do some checks to ensure since we stated that usernames and email addresses are unique, we might want to raise an error.
(when valid
(cl-forms:with-form-field-values (email username password password-verify) form
(when (mito:select-dao 'ningle-tutorial-project/models:user
(where (:or (:= :username username)
(:= :email email))))
(error "Either username or email is already registered"))
We can see from this snippet here that mito uses the SXQL Domain Specific Language for expressing SQL statements. Using the select-dao
we can query the user table and apply where
clauses using a more Lispy like syntax to check to see if an account with the username or email already exists. Such DSLs are common when interacting with SQL inside another programming language, but it's good to know that from what we learned earlier that it can handle arbitrary SQL strings or this more Lispy syntax, so you can use pure SQL syntax, if necessary.
While having this check isn't necessary, it does make the error handling somewhat nicer, as well as exploring parts of the mito api. We will also add a check to raise an error if the passwords submitted in the form do not match each other.
(when (string/= password password-verify)
(error "Passwords do not match"))
If both of these pass (and you can test different permutations of course), we can continue to using mito to create our first user object!
(mito:create-dao 'ningle-tutorial-project/models:user
:email email
:username username
:password password)
The final thing to add is that we should redirect to another route, which we can do with the ingle:redirect
function.
(ingle:redirect "/people")
You will notice that we are redirecting to a route that doesn't (yet) exist, we will write the controller below after we have finished this controller, the multiple-value-bind
section of which, when completed, looks like this:
(multiple-value-bind (valid errors)
(cl-forms:validate-form form)
(when errors
(format t "Errors: ~A~%" errors))
(when valid
(cl-forms:with-form-field-values (email username password password-verify) form
(when (mito:select-dao 'ningle-tutorial-project/models:user
(where (:or (:= :username username)
(:= :email email))))
(error "Either username or email is already registered"))
(when (string/= password password-verify)
(error "Passwords do not match"))
(mito:create-dao 'ningle-tutorial-project/models:user
:email email
:username username
:password password)
(ingle:redirect "/people"))))
Getting All Users
We will look at two final examples of using mito before we finish this tutoral, as mentioned earlier we will write a new /people
controller, which will list all the users registered in the system, and we will create a /people/:person
controller to show the details of an individual user.
Starting with the /people
controller, we create a controller like we have seen before, we then use a let
binding to store the result of (mito:retrieve-dao 'ningle-tutoral-project/model:user)
, this is how we would get all rows from a table represented by the class 'ningle-tutorial-project/models:user
. We then pass the results into a template.
(setf (ningle:route *app* "/people")
(lambda (params)
(let ((users (mito:retrieve-dao 'ningle-tutorial-project/models:user)))
(djula:render-template* "people.html" nil :title "People" :users users))))
The html for this is written as such:
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row" >
<div class="col-12">
{% for user in users %}
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title"><a href="/people/{{ user.username }}">{{ user.username }}</a></h5>
<p class="card-text"><a href="/people/{{ user.email }}">{{ user.email }}</a></p>
<p class="text-muted small"></p>
</div>
</div>
</div>
</div>
{% endfor %}
{% if not users %}
<div class="row">
<div class="col text-center">
<p class="text-muted">No users to display.</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
Getting A Single User
In our individual person view, we see how a route may have variable data, our :person
component of the URL string, this will be either a username or email, it doesn't really matter which as we can have a SQL query that will find a record that will match the :person
string with either the username or email. We also take advantage of another ingle
function, the get-param
, which will get the value out of :person
. We use a let*
binding to store the user
derived from :person
and the result of mito:select-dao
(using the person
), we then pass the user
object into a template.
As we saw before this query was used to check for the existence of a username or email address in our register
controller.
(setf (ningle:route *app* "/people/:person")
(lambda (params)
(let* ((person (ingle:get-param :person params))
(user (first (mito:select-dao
'ningle-tutorial-project/models:user
(where (:or (:= :username person)
(:= :email person)))))))
(djula:render-template* "person.html" nil :title "Person" :user user))))
And here is the template for an individual user.
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<div class="row mb-4">
<div class="col">
{% if not user %}
<h1>No users</h1>
{% else %}
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ user.username }}</h5>
<p class="card-text">{{ user.email }}</p>
<p class="text-muted small"></p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Conclusion
This was a rather large chapter and we covered a lot, looking at the different means by which to connect to a SQL database, defining models, running migrations and executing queries, of course we are just getting started but this is a massive step forward and our application is beginning to take shape. I certainly hope you have enjoyed it and found it useful!
To recap, after working your way though this tutorial you should be able to:
- Explain what a model is
- Explain what a migration is
- Write code to connect to a SQL database
- Implement a model
- Implement a migration
- Execute a migration
- Write controllers that write information to a database via a model
- Write controllers that read information from a database via a model
Github
- The link for the SQLite version of this tutorial code is available here.
- The link for the MySQL version of this tutorial code is available here.
- The link for the PostgreSQL version of this tutorial code is available here.
Resources
Paolo Amoroso — Adding window clearing and message printing to DandeGUI
@2025-04-28 10:52 · 6 days agoI continued working on DandeGUI, a GUI library for Medley Interlisp I'm writing in Common Lisp. I added two new short public functions, GUI:CLEAR-WINDOW
and GUI:PRINT-MESSAGE
, and fixed a bug in some internal code.
GUI:CLEAR-WINDOW
deletes the text of the window associated with the Interlisp TEXTSTREAM
passed as the argument:
(DEFUN CLEAR-WINDOW (STREAM)
"Delete all the text of the window associated with STREAM. Returns STREAM"
(WITH-WRITE-ENABLED (STR STREAM)
(IL:TEDIT.DELETE STR 1 (IL:TEDIT.NCHARS STR)))
STREAM)
It's little more than a call to the TEdit API function IL:TEDIT.DELETE
for deleting text in the editor buffer, wrapped in the internal macro GUI::WITH-WRITE-ENABLED
that establishes a context for write access to a window.
I also wrote GUI:PRINT-MESSAGE
. This function prints a message to the prompt area of the window associated with the TEXTSTREAM
passed as an argument, optionally clearing the area prior to printing. The prompt area is a one-line Interlisp prompt window attached above the title bar of the TEdit window where the editor displays errors and status messages.
(DEFUN PRINT-MESSAGE (STREAM MESSAGE &OPTIONAL DONT-CLEAR-P)
"Print MESSAGE to the prompt area of the window associated with STREAM. If DONT-CLEAR-P is non NIL the area will be cleared first. Returns STREAM."
(IL:TEDIT.PROMPTPRINT STREAM MESSAGE (NOT DONT-CLEAR-P))
STREAM)
GUI:PRINT-MESSAGE
just passes the appropriate arguments to the TEdit API function IL:TEDIT.PROMPTPRINT
which does the actual printing.
The documentation of both functions is in the API reference on the project repo.
Testing DandeGUI revealed that sometimes text wasn't appended to the end but inserted at the beginning of windows. To address the issue I changed GUI::WITH-WRITE-ENABLED
to ensure the file pointer of the stream is set to the end of the file (i.e -1
) prior to passing control to output functions. The fix was to add a call to the Interlisp function IL:SETFILEPTR
:
(IL:SETFILEPTR ,STREAM -1)
#DandeGUI #CommonLisp #Interlisp #Lisp
Discuss... Email | Reply @amoroso@oldbytes.space
Joe Marshall — Senior Programmers Have Nothing to Fear From AI
@2025-04-27 14:33 · 6 days agoI have, through experimentation, discovered that vibe coding in Common Lisp is not effective. But other kinds of AI coding are highly effective and have been saving me hours of work. AI is not going to replace senior programmers, but it will take over many of the tasks that junior programmers do. I’m not worried about my job, but were I a junior programmer, I’d be concerned.
Part of my job as a senior programmer is to identify tasks that are suitable for junior programmers. These tasks have certain properties:
- They are well-defined.
- They are repetitive, making them suitable for development of a program to carry them out.
- They are not too difficult, so that a junior programmer can complete them with a little help.
- They have a clear acceptance criteria, so that the junior programmer can tell when they are done.
- They have a clear abstraction boundary so that integrating the code after the junior programmer is done is not too difficult.
But because junior programmers are human, we have to consider these factors as well:
- The task must not be too simple or too boring.
- The task must be written in a popular programming language. Junior programmers don’t have the inclination to learn new programming languages.
- The task must not be time critical because junior programmers are slow.
- The task should not be core critical to the larger project. Junior programmers write crappy code, and you don’t want crappy code at the heart of your project.
Oftentimes, I find some tasks that fits many of these criteria, but then I find that I can do it myself better and faster than a junior programer could.
AI coding can handle many of the tasks that I would otherwise assign to a junior programmer. It works best when the task is well defined, not too difficult, and written in a popular language. It doesn’t care if the task is boring and repetitive. AI coding is much faster than a junior programmer, and it writes code that tends to follow standard conventions. If you can specify good abstraction barriers, the AI can do a good job of coding to them. While AI coding is not perfect, neither are junior programmers. In either case, a senior programmer needs to carefully review the code.
AI coding is not going to replace senior programmers. The AI will not generate code without a prompt, and the better the prompt, the better the generated code. Senior programmers can take a large program and break it down into smaller tasks. They can create definitions of the smaller tasks and define the acceptance criteria, the API, and the abstractions to be used. They can carefully and precisely craft the prompts that generate the code. Senior programmers are needed to drive the AI.
Which leads to the question of where senior programmers will come from if junior programmers are no longer needed. I don’t have a good answer for this.
Nicolas Martyanoff — Working with Common Lisp pathnames
@2025-04-26 18:00 · 7 days agoCommon Lisp pathnames, used to represent file paths, have the reputation of being hard to work with. This article aims to change this unfair reputation while highlighting the occasional quirks along the way.
Filenames and file paths
The distinction between filename and file paths is not always obvious. On POSIX systems, the filename is the name of the file, while a file path represents its absolute or relative location in the file system. Which also means that all filenames are file paths, but not the other way around.
Common Lisp uses the term filename for objects which are either pathnames or namestrings, both being representation of file paths. We will try to avoid confusion by using the terms filenames, pathnames and namestrings when referring to Common Lisp concepts and we will talk about file paths when referring to the language-agnostic file system concept.
Pathnames
Pathnames are an implementation-independent representation of file paths made of six components:
- an host identifying either the file system or a logical host;
- a device identifying the logical of physical device containing the file;
- a directory representing an absolute or relative list of directory names;
- a name;
- a type, a value nowadays known as file extension;
- a version, because yes file systems used to support file versioning.
While this representation might seem way too complicated —it originates from a time where the file system ecosystem was much richer— it still is suitable for modern file systems.
The make-pathname
function is used to create pathnames and lets you specificy
all components. For example the following call yields a pathname representing
the file path represented on POSIX systems by /var/run/example.pid
:
(make-pathname :directory '(:absolute "var" "run") :name "example" :type "pid")
Common Lisp functions manipulating file paths of course accept pathnames, letting you keep the same convenient structured representation everywhere, only converting from/to a native representation at the boundaries of your program.
Special characters
What happens when you construct a pathname with components containing separation
characters, e.g. a directory name containing /
on a POSIX system or a type
containing .
? According to Common Lisp 19.2.2.1.1, the behaviour is
implementation-defined; but if the implementation accepts these component values
it must handle quoting correctly.
For example:
- CLISP rejects separator characters in component values, signaling a
SIMPLE-ERROR
condition. - CCL accepts them and quotes them when converting the pathname to a
namestring. So
(namestring (make-pathname :name "foo/bar" :type "a.b"))
yields"foo\\/bar.a\\.b"
on Linux. - SBCL accepts and quotes them but does not quote
.
in type components, yielding"foo\\/bar.a.b"
for the example above. - ECL accepts them but fails to quote them when converting the pathname to a namestring.
One could wonder about which implementation, CCL or SBCL, is correct regarding
the quoting of the .
character in type strings on POSIX platforms. While
everyone understands that /
is special in file and directory names, .
is
debatable because POSIX does not mention the type extension in its definitions:
foo.txt
is the name of the file, not a combination of a name and a type. As
such, I would argue that quoting and not quoting are both correct. And as you
will realize then reading about namestrings further in this article, it is
irrelevant since namestrings are not POSIX paths.
Note that whether ECL violates the standard or not is unclear since there is no
character quoting for POSIX paths. In other words, there is no such thing as a
directory named a/b
, because it could not be referenced in a way different
from a directory named b
in a directory named a
. This behaviour derives
directly from POSIX systems treating paths as strings and not as structured
objects.
Invalid characters
The Common Lisp standard mentions special characters but is silent on the subject of invalid characters. For example POSIX forbids null bytes in filenames. But since it is not a separation character, implementations are free to deal with it as they see fit.
When testing implementations with a pathname containing a null byte using
(make-pathname :name (string (code-char 0)))
, CCL, SBCL and ECL accept it
while CLISP signals an error mentioning an illegal argument.
I am not convinced by CLISP’s behaviour since null bytes are only invalid in POSIX paths, not in Common Lisp filenames, meaning that the error should occur when the pathname is internally converted to a format usable by the operating system.
Pathname component case
A rarely mentioned property of pathnames is the support for case conversion.
MAKE-PATHNAME
and function returning pathname components (e.g.
PATHNAME-TYPE
) support a :CASE
argument, either :COMMON
or :LOCAL
indicating how how to handle character case in strings.
With :LOCAL
—which is the default value—, these functions assume that
component strings are already represented following the conventions of the
underlying operating system. It also dictates that if the host only supports one
character case, strings must be returned converted to this case.
With :COMMON
, these functions will use the default (customary) case of the
host if the string is provided all uppercase, and the opposite case if the
string is provided all lowercase. Mixed case strings are not transformed.
These behaviours are not intuitive and made much more sense at a time where some file systems only supported one specific case. You should probably stay away from component case handling unless you really know what you are doing.
On a personal note, as someone running Linux and FreeBSD, I am curious about the behaviour of various implementations on Windows and MacOS since both NTFS and APFS are case insensitive.
Unspecific components
While all components can be null, some of them can be :UNSPECIFIC
(which ones
is implementation-defined). The only real use case for :UNSPECIFIC
is to
affect the behaviour of MERGE-PATHNAMES
: if a component is null, the function
uses the value of the component in the pathname passed as the :DEFAULTS
argument; if a component is :UNSPECIFIC
, the function uses the same value in
the resulting pathname.
For example:
(merge-pathnames (make-pathname :name "foo")
(make-pathname :type "txt"))
yields the "foo.txt"
namestring, but
(merge-pathnames (make-pathname :name "foo" :type :unspecific)
(make-pathname :type "txt"))
yields "foo"
.
Unfortunately the inability to rely on its support for specific component types (since it is implementation-defined) makes it interesting more than useful.
Namestrings
Namestrings are another represention for file paths. While pathnames are
structured objects, namestrings are just strings. The most important aspect of
namestrings is that unless they are logical namestrings (something we will cover
later), the way they represent paths is implementation-defined (c.f. Common Lisp
19.1.1 Namestrings as Filenames). In other words the namestring for the file
foo
of type txt
in directory data
could be data/foo.txt
. Or
data\foo.txt
. Or data|foo#txt
. Or any other non-sensical representation.
Fortunately implementations tend to act rationally and use a representation as
similar as possible to the one of their host operating system.
One should always remember that even though namestrings look and feel like
paths, they are still a representation of a Common Lisp pathname, meaning that
they may or may not map to a valid native path. The most obvious example would
be a pathname whose name is the null byte, created with (make-pathname :name (string (code-char 0)))
, whose namestring is a one character string that has no
valid native representation on modern operating systems.
Pathnames can be converted to namestrings using the NAMESTRING
function, while
namestrings can be parsed into pathnames with PARSE-NAMESTRING
. The #P
reader macro uses PARSE-NAMESTRING
to read a pathname. As such,
#P"/tmp/foo.txt"
is identical to #.(parse-namestring '"/tmp/foo.txt")
.
Note that most functions dealing with files will accept a pathname designator, i.e. either a pathname, a namestring or a stream.
Native namestrings
An unfortunately missing feature from Common Lisp is the ability to parse native namestrings, i.e. paths that use the representation of the underlying operating system.
To understand why it is a problem, let us take *.txt
, a perfectly valid
filename at least on any POSIX platform. In Common Lisp, you can construct a
pathname representing this filename with (make-pathname :name "*" :type "txt")
. No problem whatsoever. However the "*.txt"
namestring actually
represents a pathname whose name component is :WILD
. There is no namestring
that will return this pathname when passed to PARSE-NAMESTRING
.
As a result, when processing filenames coming from the external world (a command line argument, a list of paths in a document, etc.), you have no way to handle those that contain characters used by Common Lisp for wild components.
There is no standard way of solving this issue. Some implementations provide
functions to parse native namestrings, e.g. SBCL with
SB-EXT:PARSE-NATIVE-NAMESTRING
or CCL with CCL:NATIVE-TO-PATHNAME
. If you
use ASDF, you can also use
UIOP:PARSE-NATIVE-NAMESTRING
.
Wildcards
Up to now pathnames may have looked like a slightly unusual representation for paths. But we are just getting started.
Pathname can be wild, meaning that they contain one or more wild components.
Wild components can match any value. All components can be made wild with the
special value :WILD
. Directory elements also support :WILD-INFERIORS
which
matches one or more directory levels.
As such
(make-pathname :directory '(:absolute "tmp" :wild) :name "foo" :type :wild)
is equivalent to the /tmp/*/foo.*
POSIX glob pattern, while
(make-pathname :directory '(:absolute "tmp" :wild-inferiors "data" :wild)
:name :wild :type :wild)
is equivalent to /tmp/**/data/*/*.*
.
Wild pathnames only really make sense for the DIRECTORY
function which returns
files matching a specific pathname.
Logical pathnames
We currently have talked about pathnames representing either paths to physical files or pattern of filenames. Logical pathnames go further and let you work with files in a location-independent way.
Logical pathnames are based on logical hosts, set as pathname host components. Logical pathnames can be passed around and manipulated as any other pathnames; when used to access files, they are translated to a physical pathname, i.e. a pathname referring to an actual file in the file system.
SBCL uses logical pathnames for source file locations. While SBCL is shipped
with its source files, their actual location on disk depends on how the software
was installed. Instead of manually merging pathnames with a base directory value
everywhere, SBCL uses the SYS
logical host to map all pathnames whose
directory starts with SRC
to the actual location on disk. For example on my
machine:
(translate-logical-pathname "SYS:SRC;ASSEMBLY;MASTER.LISP")
yields #P"/usr/share/sbcl-source/src/assembly/master.lisp"
.
Another example would be CCL which maps pathnames with the HOME
logical host
to subpaths of the home directory of the user.
Note that logical hosts are global to the Common Lisp environment. While SYS
is reserved for the implementation, all other hosts are free to use by anyone.
To avoid collisions, it is a good idea to name hosts after their program or
library.
Logical namestrings
Logical namestrings are implementation-independent, meaning that you can safely use them in your programs without wondering about how they will be interpreted. Their syntax, detailed in section 19.3.1 of the Common Lisp standard, is quite different from usual POSIX paths. The host is separated from the rest of the path by a colon character, and directory names are separated by semicolon characters.
For example "SOURCE:SERVER;LISTENER.LISP"
is the logical namestring equivalent
of the /server/listener.lisp
POSIX path for the SOURCE
logical host.
The astute reader will notice the use of uppercase characters in logical namestrings. It happens that the different parts of a logical namestring are defined as using uppercase characters, but that the implementation translates lowercase letters to uppercase letters when parsing the namestrings (c.f. Common Lisp 19.3.1.1.7). We use the canonical uppercase representation for clarity.
Translation
Translation is controlled by a table that maps logical hosts to a list of pattern (wild pathnames or namestrings) and their associated wild physical pathnames.
One can obtain the list of translations for a logical host with
LOGICAL-PATHNAME-TRANSLATIONS
and update it with (SETF LOGICAL-PATHNAME-TRANSLATIONS)
. Each translation is a list where the first
element is a logical pathname or namestring (usually a wild pathname) and the
second element is a pathname or namestring to translate into.
The translation process looks for the first entry that satisfies
PATHNAME-MATCH-P
, which is guaranteed to behave in a way consistent with
DIRECTORY
. When there is match, the translation processes replaces
corresponding patterns for each components.
And of course if translation results in a logical pathname, it will be recursively translated until a physical pathname is obtained.
A simple example would be the use of a logical host referring to a temporary directory. This lets a program manipulates temporary pathnames without having to know their actual physical location, the translation process being controlled in a single location.
(setf (logical-pathname-translations "tmp")
(list (list (make-pathname :host "tmp"
:directory '(:absolute :wild-inferiors)
:name :wild :type :wild)
(make-pathname :directory '(:absolute "var" "tmp" :wild-inferiors)
:name :wild :type :wild))))
or if we were to use namestrings:
(setf (logical-pathname-translations "tmp")
'(("TMP:**;*.*.*" "/var/tmp/**/*.*")))
Translating pathnames or namestrings using the TMP
logical host yields the
expected results. For example (translate-logical-pathname "TMP:CACHE;DATA.TXT")
yields #P"/var/tmp/cache/data.txt"
.
Caveats
While logical pathnames are an elegant abstraction, they are plagued by multiple issues that make them hard to use correctly and in a portable way.
Logical namestring components can only contain letters, digits and hyphens (or
the *
and **
sequences for wild namestrings). This limitation probably comes
from a need to be compatible with all existing file systems, but it can be a
showstopper if one needs to refer to files whose naming scheme is not controlled
by the program.
Namestring parsing is confusing: calling PARSE-NAMESTRING
on an invalid
namestring (because it contains invalid characters or because the host is not a
known logical host) will not fail. Instead the string will be parsed as a
physical namestring, introducing silent bugs. The LOGICAL-PATHNAME
can be used
to validate logical pathnames and namestrings.
The way translation converts between both pathname patterns is unclear. It is not specified by the Common Lisp standard. Debugging patterns can quickly become very frustrating, especially with implementations unable to produce quality error diagnostics.
Finally, the behaviour of logical pathnames with other functions is rarely obvious, leading to frustrating debugging sessions.
They nevertheless are a unique and helpful feature for very specific use cases.
Recipes
Resolving a path
Files are accessible through multiple paths. For example, on POSIX systems,
foo/bar/baz.txt
, foo/bar/../bar/baz.txt
refer to the same file. If your
operating system and file system support symbolic links, you can refer to the
same physical file from multiple links, themselves being files.
It is sometimes useful to obtain the canonical path of a file. On POSIX systems,
the realpath
function serves this purpose. In Common Lisp, this canonical path
is called truename, and the TRUENAME
function returns it.
Transforming paths
The :DEFAULTS
option of MAKE-PATHNAME
is useful to construct a pathname that
is a variation of another pathname. When a component passed to MAKE-PATHNAME
is null, the value is taken from the pathname passed with :DEFAULTS
.
For example to create the pathname of a file in the same directory as another pathname:
(make-pathname :name "bar"
:defaults (make-pathname :directory '(:absolute "tmp") :name "foo"))
Or to create a wild pathname matching the same file names but with any extension:
(make-pathname :type :wild
:defaults (make-pathname :name "foo" :type "txt"))
Or to obtain a pathname for the directory of a file:
(make-pathname :name nil
:defaults (make-pathname :directory '(:relative "a" "b" "c")
:name "foo"))
Joining two paths
Joining (or concatenating) two paths can be done with MERGE-PATHNAMES
. In
general calling (MERGE-PATHNAMES PATH1 PATH2)
returns a new pathname whose
components are taken either from PATH1
when they are not null, or from PATH2
when they are. As a special case, if the directory component of PATH1
is
relative, the directory component of the result pathname is the concatenation of
the directory components of both paths.
In other words
(merge-pathnames (make-pathname :directory '(:relative "x" "y"))
(make-pathname :directory '(:absolute "a" "b" "c")))
yields "/a/b/c/x/y/"
but
(merge-pathnames (make-pathname :directory '(:absolute "x" "y"))
(make-pathname :directory '(:absolute "a" "b" "c")))
yields "/x/y/"
.
Finding files
The DIRECTORY
function returns files matching a pathname, wild or not.
If the pathname is not wild, DIRECTORY
returns a list of one or zero element
depending on whether a file exists at this location or not.
If the pathname is wild, DIRECTORY
behaves similarly to POSIX globs. Due to
the way pathnames are structured, with the name and type being two different
components, a common error is to specify a wild name without a type. In this
case, DIRECTORY
will not return any file with an extension (since their
pathname has a non-null type). To match all files with any extension, set both
the name and the type to :WILD
.
Another interesting possibility is to only match directories. Directories are
represented by pathnames with a non-null directory component and a null name
component. Therefore to find all directories in /tmp
(top-level only):
(directory (make-pathname :directory '(:absolute "tmp" :wild)))
Note that DIRECTORY
returns truenames, i.e. pathnames representing the
canonical location of the files. An unexpected consequence is that the function
will resolve symlinks. Since the Common Lisp standard explicitely allows extra
optional arguments, some implementations have a way to disable symlink
resolving, e.g. SBCL with :RESOLVE-SYMLINKS
or CCL with :FOLLOW-LINKS
.
Resolving tildes in paths
It is commonly believed that tilde characters in paths is a universal feature. It is not. Tilde prefixes are defined in POSIX in the context of the shell (cf. POSIX 2017 2.6.1 Tilde Expansion) and are only supported in very specific locations.
To obtain the path of a file relative to the home directory of the current user,
use the USER-HOMEDIR-PATHNAME
function.
For example:
(merge-pathnames (make-pathname :directory '(:relative ".emacs.d")
:name "init" :type "el")
(user-homedir-pathname))
Joe Marshall — Get Over It (ai content)
@2025-04-25 07:00 · 9 days agoI'm tired of people complaining about all the parentheses in Lisp, so I told Gemini to vent for me. This came out pretty good.
I suppose I'm guilty of contributing to the glut of AI slop, but while the text and tone are generated, the core idea and sentiment is mine, so it isn’t pure slop.
Alright, let's drop the hand-holding. You — yes, you — the one still whimpering about the parentheses in Lisp. It's time someone told you bluntly: Get over it.
Your constant refrain about "too many parentheses" isn't some profound insight into language design; it's a confession of intellectual laziness. It screams that you encountered something different, something that didn't conform to the aesthetic you were spoon-fed, and you threw up your hands rather than engage your brain.
Are parentheses really an insurmountable cognitive barrier? Are matching pairs of symbols truly beyond your capacity to track? Let's be honest. The core issue isn't difficulty; it's unfamiliarity coupled with a stubborn refusal to adapt. You've decided what code ought to look like, and anything else is dismissed as "hard" or "ugly" without genuine effort.
Thousands upon thousands of programmers — people with brains presumably no bigger than yours — have not only managed Lisp's syntax but have thrived with it. They've built complex, powerful systems. They didn't possess some secret anti-parenthesis superpower; they simply possessed the willingness to learn and adapt. They saw the structure, the consistency, the power behind the syntax, instead of getting stuck on the superficial shape of the characters.
Your complaint isn’t about usability; it’s about preference, and a remarkably shallow one at that. It’s like refusing to read a brilliant novel because you don’t like the font. It demonstrates a lack of curiosity, an unwillingness to look past the surface.
So, here's the unvarnished truth: The parentheses aren't the problem. Your rigid thinking is. Your demand that every language cater to your pre-existing biases is.
Stop whining. Stop making excuses based on trivial syntax. Either put in the minimal mental effort required to see the structure beneath the curves, recognise the direct representation of the abstract syntax tree, and understand why it is the way it is - or don't. But don't pretend your aesthetic preference is a legitimate technical critique.
The Lisp world isn't waiting for you to overcome your parenthesis phobia. It's moving on, building things, leveraging a unique and powerful paradigm. You can either engage your intellect and join in, or you can stay on the sidelines complaining about the punctuation.
Frankly, it's time to grow up and get over the parentheses.
Marco Antoniotti — Emacs Lisp Programming with DeepSeek: A New Widget
@2025-04-24 20:52 · 9 days agoThe Emacs widget
library is useful; alas its guts are
... semi-documented and most of its inner working a bit mysterious.
I wanted a column widget where I could insert and remove a few
"line-like" widgets. The editable-list
widget does not cut it
(too many extra things: the INS and DEL buttons) and the
group
widget is too inflexible.
After too much time trying to understand all the intricacies of the
widget
library (see my rant in
my previous blog post,
which perfectly applies in this case) I asked DeepSeek to help me
out. The result, the dynamic-group
widget (after several
iterations and mistakes on part of DeepSeek) is below. It works
satisfactorlly, although it could be improved by anybody with a
better understanding of the widget
library. What is does is to
manage a colimn of line-like widgets adding and removing from the
end of the :children
list.
Check the demo-dynamic-group
for a test run.
It has been fun. Although I still want a better widget! That's why I am posting this for anybody to pitch in. Any help is welcome.
BTW. There still are some warts in the code. Can you spot them?
;;; -*- Mode: ELisp; lexical-binding: t -*- ;;; emc-dynamic-group.el ;;; ;;; `flycheck-mode' does not like the above. `flycheck-mode' is wrong. ;;; Code: (require 'widget) (require 'wid-edit) (define-widget 'dynamic-group 'default "A container widget that dynamically manages child widgets in a column." :format "%v" :value () :tag "Dynamic Group" :args nil ;; Core widget methods :create (lambda (widget) (let ((inhibit-read-only t)) (widget-put widget :from (point)) (dolist (child (reverse (widget-get widget :children))) (widget-create child)) (widget-put widget :to (point)))) :value-get (lambda (widget) (mapcar (lambda (child) (widget-apply child :value-get)) (widget-get widget :children))) :value-set (lambda (widget value) (widget-put widget :value value)) :value-delete (lambda (widget) (dolist (child (widget-get widget :children)) (widget-apply child :value-delete))) :validate (lambda (widget) (let ((children (widget-get widget :children))) (catch :invalid (dolist (child children) (when (widget-apply child :validate) (throw :invalid child))) nil))) ) (defun dynamic-group-add (widget type &rest args) "Add a new widget (of TYPE and ARGS to the WIDGET group." (let ((inhibit-read-only t)) (save-excursion (goto-char (widget-get widget :to)) (let ((child (apply 'widget-create (append (list type) args)))) (widget-put widget :children (cons child (widget-get widget :children))) (widget-put widget :to (point)) (widget-value-set widget (cons (widget-value child) (widget-value widget))))) (widget-setup))) (defun dynamic-group-remove (widget) "Remove the last widget from the WIDGET group." (when-let ((children (widget-get widget :children))) (let ((inhibit-read-only t) ;; (child (car children)) ) (save-excursion (goto-char (widget-get widget :from)) (delete-region (point) (widget-get widget :to)) (widget-put widget :children (cdr children)) (dolist (c (reverse (widget-get widget :children))) (widget-create c)) (widget-put widget :to (point)) (widget-value-set widget (mapcar 'widget-value (widget-get widget :children))) (widget-setup))))) (defun demo-dynamic-group () "Test the dynamic-group widget." (interactive) (switch-to-buffer "*Dynamic Group Demo*") (kill-all-local-variables) (let ((inhibit-read-only t)) (erase-buffer) (widget-insert "* Dynamic Group Demo\n\n") ;; Now I create the `dynamic-group'. (let ((group (widget-create 'dynamic-group))) (widget-insert "\n") ;; The rest are just two buttons testing the widget's behavior, ;; invoking`dynamic-group-add' and `dynamic-group-remove'. (widget-create 'push-button :notify (lambda (&rest _) (dynamic-group-add group 'string :format "Text: %v\n" :value (format "Item %d" (1+ (length (widget-get group :children)))))) "(+) Add Field (Click Anywhere)") (widget-insert " ") (widget-create 'push-button :notify (lambda (&rest _) (dynamic-group-remove group)) "(-) Remove Last") (widget-insert "\n")) ;; Wrap everything up using the `widget-keymap' and `widget-setup' ;; functions. (use-local-map widget-keymap) (widget-setup))) (provide 'emc-dynamic-group)
'(cheers)
Joe Marshall — Lisp Debugger Wins
@2025-04-24 18:00 · 9 days agoI'm gathering statistics from thousands of pull requests in GitHub. I wrote a little Lisp program to do that. It's taking a long time because it has to make a lot of API calls to GitHub and the calls are rate limited.
After about half an hour, I got an unexpected error. A user who made the pull request I was looking at had deleted their account. No problem. I used the Lisp debugger to walk up the stack to a convenient frame and returned NIL from that frame, causing that particular PR to be skipped. The program continued running from where it left off. I didn't have to restart from the beginning and lose a half hour of work.
The Lisp debugger for the win!
Gábor Melis — PAX and DRef v0.4
@2025-04-23 00:00 · 11 days agoVersion 0.4 of PAX, the documentation system, and DRef, the definition reifier, was released. There were large refactorings, bug fixes, minor features, cosmetics, documentation and performance improvements too numerous to list. Here is a summary of the new features and notable changes.
DRef now supports
DTYPE
s, which allow filteringDEFINITIONS
andDREF-APROPOS
results according to the locative type hierarchy:(definitions 'print) ==> (#<DREF PRINT FUNCTION> --> #<DREF PRINT (UNKNOWN (:DEFOPTIMIZER PRINT SB-C:DERIVE-TYPE))> --> #<DREF PRINT (UNKNOWN --> (DECLAIM PRINT --> SB-C:DEFKNOWN))>)
(definitions 'print :dtype '(and t (not unknown))) ==> (#<DREF PRINT FUNCTION>)
The
AND T
bit restricts the query to definitions in the running Lisp. The top of theDTYPE
hierarchy isDREF:TOP
, which includes external definitions such as theCLHS
, that comes with PAX:(definitions 'print :dtype '(not unknown)) ==> (#<DREF PRINT (CLHS FUNCTION)> #<DREF PRINT FUNCTION>)
(dref-apropos "method" :package :dref :external-only t :dtype 'class) ==> (#<DREF METHOD CLASS> #<DREF METHOD-COMBINATION CLASS>)
The locative type hierarchy can be queried programmatically, and this information is included in their documentation (see for example the
GENERIC-FUNCTION
locative type).The PAX Live Home Page better supports exploration without having to leave the browser.
It lists packages grouped by ASDF systems that define them (when this can be determined from the source locations).
It links to apropos pages for each locative type.
It has an input box for looking up documentation right from the browser (as if with
mgl-pax-document
from Emacs).It has an input box for looking up apropos right from the browser (as if with
mgl-pax-apropos
from Emacs).The web server can be started without Emacs.
Completion of names and locatives in Emacs is much improved.
New aliases were added to the
CLHS
pages documenting format directives (e.g.~F
), standard macro characters (#A
) and loop keywords (sum
,:sum
,loop:sum
), so that one can justC-.
(mgl-pax-document
) them. See the documentation of theCLHS
locative.The DRef extension api has been cleaned up.
Paolo Amoroso — DandeGUI, a GUI library for Medley Interlisp
@2025-04-21 19:17 · 12 days agoI'm working on DandeGUI, a Common Lisp GUI library for simple text and graphics output on Medley Interlisp. The name, pronounced "dandy guy", is a nod to the Dandelion workstation, one of the Xerox D-machines Interlisp-D ran on in the 1980s.
DandeGUI allows the creation and management of windows for stream-based text and graphics output. It captures typical GUI patterns of the Medley environment such as printing text to a window instead of the standard output. The main window of this screenshot was created by the code shown above it.
The library is written in Common Lisp and exposes its functionality as an API callable from Common Lisp and Interlisp code.
Motivations
In most of my prior Lisp projects I wrote programs that print text to windows.
In general these windows are actually not bare Medley windows but running instances of the TEdit rich-text editor. Driving a full editor instead of directly creating windows may be overkill, but I get for free content scrolling as well as window resizing and repainting which TEdit handles automatically.
Moreover, TEdit windows have an associated TEXTSTREAM
, an Interlisp data structure for text stream I/O. A TEXTSTREAM
can be passed to any Common Lisp or Interlisp output function that takes a stream as an argument such as PRINC
, FORMAT
, and PRIN1
. For example, if S
is the TEXTSTREAM
associated with a TEdit window, (FORMAT S "~&Hello, Medley!~%")
inserts the text "Hello, Medley!" in the window at the position of the cursor. Simple and versatile.
As I wrote more GUI code, recurring patterns and boilerplate emerged. These programs usually create a new TEdit window; set up the title and other options; fetch the associated text stream; and return it for further use. The rest of the program prints application specific text to the stream and hence to the window.
These patterns were ripe for abstracting and packaging in a library that other programs can call. This work is also good experience with API design.
Usage
An example best illustrates what DandeGUI can do and how to use it. Suppose you want to display in a window some text such as a table of square roots. This code creates the table in the screenshot above:
(gui:with-output-to-window (stream :title "Table of square roots")
(format stream "~&Number~40TSquare Root~2%")
(loop
for n from 1 to 30
do (format stream "~&~4D~40T~8,4F~%" n (sqrt n))))
DandeGUI exports all the public symbols from the DANDEGUI
package with nickname GUI
. The macro GUI:WITH-OUTPUT-TO-WINDOW
creates a new TEdit window with title specified by :TITLE
, and establishes a context in which the variable STREAM
is bound to the stream associated with the window. The rest of the code prints the table by repeatedly calling the Common Lisp function FORMAT
with the stream.
GUI:WITH-OUTPUT-TO-WINDOW
is best suited for one-off output as the stream is no longer accessible outside of its scope.
To retain the stream and send output in a series of steps, or from different parts of the program, you need a combination of GUI:OPEN-WINDOW-STREAM
and GUI:WITH-WINDOW-STREAM
. The former opens and returns a new window stream which may later be used by FORMAT
and other stream output functions. These functions must be wrapped in calls to the macro GUI:WITH-WINDOW-STREAM
to establish a context in which a variable is bound to the appropriate stream.
The DandeGUI documentation on the project repository provides more details, sample code, and the API reference.
Design
DandeGUI is a thin wrapper around the Interlisp system facilities that provide the underlying functionality.
The main reason for a thin wrapper is to have a simple API that covers the most common user interface patterns. Despite the simplicity, the library takes care of a lot of the complexity of managing Medley GUIs such as content scrolling and window repainting and resizing.
A thin wrapper doesn't hide much the data structures ubiquitous in Medley GUIs such as menus and font descriptors. This is a plus as the programmer leverages prior knowledge of these facilities.
So far I have no clear idea how DandeGUI may evolve. One more reason not to deepen the wrapper too much without a clear direction.
The user needs not know whether DandeGUI packs TEdit or ordinary windows under the hood. Therefore, another design goal is to hide this implementation detail. DandeGUI, for example, disables the main command menu of TEdit and sets the editor buffer to read-only so that typing in the window doesn't change the text accidentally.
Using Medley Common Lisp
DandeGUI relies on basic Common Lisp features. Although the Medley Common Lisp implementation is not ANSI compliant it provides all I need, with one exception.
The function DANDEGUI:WINDOW-TITLE
returns the title of a window and allows to set it with a SETF
function. However, the SEdit structure editor and the File Manager of Medley don't support or track function names that are lists such as (SETF WINDOW-TITLE)
. A good workaround is to define SETF
functions with DEFSETF
which Medley does support along with the CLtL macro DEFINE-SETF-METHOD
.
Next steps
At present DandeGUI doesn't do much more than what described here.
To enhance this foundation I'll likely allow to clear existing text and give control over where to insert text in windows, such as at the beginning or end. DandeGUI will also have rich text facilities like printing in bold or changing fonts.
The windows of some of my programs have an attached menu of commands and a status area for displaying errors and other messages. I will eventually implement such menu-ed windows.
To support programs that do graphics output I plan to leverage the functionality of Sketch for graphics in a way similar to how I build upon TEdit for text.
Sketch is the line drawing editor of Medley. The Interlisp graphics primitives require as an argument a DISPLAYSTREAM
, a data stracture that represents an output sink for graphics. It is possible to use the Sketch drawing area as an output destination by associating a DISPLAYSTREAM
with the editor's window. Like TEdit, Sketch takes care of repainting content as well as window scrolling and resizing. In other words, DISPLAYSTREAM
is to Sketch what TEXTSTREAM
is to TEdit.
DandeGUI will create and manage Sketch windows with associated streams suitable for use as the DISPLAYSTREAM
the graphics primitives require.
#DandeGUI #CommonLisp #Interlisp #Lisp
Discuss... Email | Reply @amoroso@oldbytes.space
Joe Marshall — Well This Was Interesting
@2025-04-20 20:15 · 13 days agoI was playing with notebooklm.google.com and for fun I entered a few of my recent blog posts. I asked it to generate a conversation. The AI generated what sounds a lot like a podcast discussing my blog. One host sounds a bit like Mike Rowe. They get a lot of the detail right, but there are a couple of glaring mistakes. (Read - EVIL - Print - Loop) I suppose I am contributing to the soup of AI slop, but I was suprised at how natural this sounds. Podcast
I also tried giving it some text files about Tic Tac Toe representations. It generated a remarkably good “Deep Dive” that really sounds as if it has an understanding of the text: Deep Dive
There are some “tells” that this is AI and not human, but I wouldn't be surprised if this could fool the average layman. In a few years, it’s going to be really hard to detect, though.
Joe Marshall — Stupid reader tricks
@2025-04-18 14:38 · 15 days agoHere are some stupid reader tricks for Lisp. I've tested them on SBCL, and they are of questionable portability and utility.
Run Shell Commands from the Lisp Prompt
(set-macro-character #\! (lambda (stream char) (declare (ignore stream char)) (uiop:run-program (read-line stream) :output *standard-output*)) t) > !ls -als total 4068 4 drwxr-x--- 21 jrm jrm 4096 Apr 18 06:42 . 4 drwxr-xr-x 4 root root 4096 Mar 22 17:27 .. 1900 -rwx--x--x 1 jrm jrm 1940604 Apr 17 19:10 .bash_history 4 -rw-r--r-- 1 jrm jrm 220 Mar 19 12:16 .bash_logout 8 -rw-r--r-- 1 jrm jrm 4961 Apr 1 11:13 .bashrc 4 drwx------ 6 jrm jrm 4096 Mar 21 07:52 .cache 0 lrwxrwxrwx 1 jrm jrm 51 Mar 24 05:20 .config -> /mnt/c/Users/JosephMarshall/AppData/Roaming/.config 0 lrwxrwxrwx 1 jrm jrm 50 Mar 26 03:12 .emacs -> /mnt/c/Users/JosephMarshall/AppData/Roaming/.emacs 4 drwx------ 6 jrm jrm 4096 Apr 17 12:13 .emacs.d ... etc ... >
Make λ an alias for lambda
(set-macro-character #\λ (lambda (stream char) (declare (ignore stream char)) 'cl:lambda) t) > ((λ (x) (+ x 4)) 3) 7
If you do this you might want to add a print function for the lambda symbol:
(defmethod print-object ((obj (eql 'cl:lambda)) stream) ;; doubt this is portable (format stream "λ")) > '(λ (x) (+ x 4)) (λ (x) (+ x 4)) > (symbol-name (car *)) "LAMBDA"
Joe Marshall — DES Machine
@2025-04-18 06:13 · 16 days agoThe MIT CADR Lisp Machine had a number of static RAMs that were used in the processor for various things such as state machines and register files. The core parts of the LMI Lambda Lisp Machine were similar to the CADR (similar enough that they could run the same microcode) but technology had advanced such that the static RAM chips were typically double the size of the CADR's. The LMI Lambda thus had twice as many registers as the CADR, but because there weren't any extra bits in the instruction set, you couldn't address half of them. The extra address bit from the RAM was wired to a status register. So the LMI Lambda had, in effect, two banks of registers which you could swap between by toggling the bit in the status register. This was not normally used — the microcode would set the bit to zero and leave it there.
A friend of mine was interested in security and he had written a high performance version of the encryption algorithm used by Unix to encrypt passwords. He was experimenting with dictionary attacks against passwords and one bottleneck was the performance of the password encryption algorithm.
It occurred to me that I could store the DES S-boxes in the alternate register bank of the LMI Lambda. With some special microcode, I could turn an LMI Lambda into a DES machine that could churn through a dictionary attack at a high speed. I added a special Lisp primitive that would swap the register banks and then run several hundred rounds of the DES algorithm before swapping back and returning to Lisp. Then I wrote a Lisp program that would feed a dictionary into the DES primitives when the processor was idle.
I was able to discover a few passwords this way, but I was more interested in the proof of concept that the actual results. A microcoded DES machine would work, but you'd get better performance out of dedicated hardware.
Thomas Fitzsimmons — Lisp ELF toolkit
@2025-04-18 04:35 · 16 days agoI recently needed to generate an ELF
binary with both RPATH
and RUNPATH
entries. I could not figure out how to produce this using linker command line arguments.
I was considering attempting a linker script, but first I switched to my Lisp
REPL
buffer 1 and found that (ql:quickload "elf")
loaded a promising-looking Common Lisp ELF
library.
I created a stub library with RPATH
using gcc
and an empty C
file, then loaded it with (elf:read-elf)
.
With the SLIME
inspector (M-x slime-inspect
) I could traverse the structure of the ELF
headers. I eventually found the RPATH
entry.
In the REPL
I built up a function to search for RPATH
then push
a new RUNPATH
entry alongside it.
It turned out the ELF
library had no support for the RUNPATH
entry, so I redefined its dyn-tag
dictionary to include it.
After adding RUNPATH
, I wrote the modified ELF
structures to a file using (elf:write-elf)
. The generated ELF
file sufficed for the test case.
I thought this was an interesting use case to share, demonstrating unique properties of the Lisp
environment. I published the result (I realize now I should have written generate-example-library.sh
in Lisp
instead of shell!; oh well).
- Which I have been trying to keep open lately, inspired by this post.
Joe Marshall — Mea Culpa
@2025-04-13 15:35 · 20 days agoOH NO! There's something wrong on the Internet!
It is often me. Mea culpa. Mea maxima culpa. I'm only human.
But this is a blog, not a reference manual. I use it to organize my thoughts, get things off my chest, provoke discussion, maybe entertain a few fellow hackers, and show others how much fun I'm having playing with computers. Accuracy and precision aren't the primary objectives although I try not to be egregiously incorrect.
Mostly I see people misinterpreting something I say casually. I gloss over some details that some find important but I consider boring. I make the first-order error of assuming everyone has the same backgroud as I do and will interpret what I say in the way I had in mind. Clearly this isn't the case, yet I persist in thinking this way. Oh well.
I'll try to correct errors that are brought to my attention and will elaborate things in more detail if asked, but if my blog irritates you with its broad generalizations, inattention to detail, omission of specifics, and things that are just plain wrong, why are you reading it?
Joe Marshall — Emacs and Lisp
@2025-04-13 14:54 · 20 days agoThe first editor I learned how to use was TECO on a line printer. You’d print the line of code you were on, then you’d issue commands to move the cursor around. You tried to avoid printing the line because that would be wasting paper. So you’d move the cursor around blind until you thought you got it to where you wanted, and then you’d start inserting characters. When you thought you had it, you’d print out the edited line.
Another undergrad saw me struggling with this and asked why I
wasn’t using vi
. I had never heard of vi
and I was amazed that you could view the code on the screen and move
your cursor around visually before going into insert mode and adding
text. With vi
I was orders of magnitude more
productive than with TECO.
When I came to the ’tute in the early 80s, I found that computer accounts were only routinely issued to students taking computer science courses. I hadn’t decided on a computer science major, so I didn’t have an account. However the Student Information Processing Board would give out a Multics account to interested students, so I signed up for that. The Multics terminal was in the basement and it had a dial up connection with an acoustically coupled modem: two rubber cups that the handset would cradle in.
Everyone at the AI Lab used a home grown editor called emacs, and
there was a Multics port. I abandoned vi
and learned
emacs. The paradigm was different, but I didn’t see one as superior
to the other. When I declared computer science as my major, I got
an account at the AI Lab on the Decsystem 20 machine. The editor
was TECO with the editor macros (emacs) package loaded.
When I took S&ICP, we had a lab with HP9836 “Chipmunks” running MIT Scheme. The Chipmunks were PCs, not time-shared, and each acted as its own Scheme machine, complete with an emacs clone for editing the code.
At the AI Lab, there were a couple of machines running ITS, the Incompatible Timesharing System. You could run Maclisp on them, but Maclisp was such a pig, using over a megabyte of RAM, that a couple of instances of Maclisp would bring the machine to its knees. The Lab had developed the Lisp Machine, a single user computer that would run ZetaLisp (the successor to Maclisp). In addition to Lisp, the Lisp machine ran the ZWEI editor. Zwei Was Eine Initially, and Eine Is Not Emacs, but rather an emacs clone written in Zetalisp.
ZWEI was integrated with the Lisp environment. You could insert Lisp objects in the ZWEI editor and their printed representation would appear in the edited text. The printed represention was mouse sensitive and had its own context menu.
If you weren’t using a Lisp Machine, your options were Unipress Emacs and Gosling Emacs which you could run on this new OS called “Unix”
Around this time (in the late 80s) a hacker named Stallman decided to write a new OS. His first task was to write an editor and he decided on a new version of Emacs written in its own Lisp dialect.
If you wanted to use Lisp, you interacted with it via Emacs.
These days, I use GNU Emacs and I load up the sly package. Sly is a slime fork and it turns GNU Emacs into an IDE for a Lisp running in another process. The interface gives you much of what you used to get when using ZWEI on the Lisp machine. You can evaluate subexpressions, macroexpand and compile programs, gather output, and run the Lisp debugger from within GNU emacs.
Emacs and Lisp co-evolved at MIT and Emacs has always been used as
a front-end to Lisp. I’ve never gone back to vi
, and
when I’ve had to use it I’ve found it frustrating (but this is
because I have forgotten everything about how to use it).
I understand that some people find Emacs impossible to use and have
a strong preference for vi
. Is the experience of
hacking Lisp in vi
any good?
Joe Marshall — Writing Your OS in Lisp
@2025-04-12 23:37 · 21 days agoHow to write an OS in Lisp is beyond the scope of a single post in this blog, and I don‘t have a series prepared. However, I can drop some hints of how you can overcome some problems.
If you are writing your OS in Lisp, it helps to have some idea of how
Lisp compiles your program to machine code.
The disassemble
function prints out the disassembly of
a Lisp function. It is obviously implementation dependent.
The inner dispatch of the compiler, when it is called, will be given a target location in which to deposit the result of evaluating the compiled expression. The target location given to the compiler will typically be a register, the top of stack, a fixed offset within the stack, or a global memory location, that sort of thing. The compiler should generate code that ultimately results in the value of the expression ending up in the target location.
A number of Lisp primitive functions can be implemented as a single CPU instruction. (Alternatively, you can create quirky Lisp “CPU subprimitive” functions out of each of the non-jump CPU instructions.) When compiling a function call to such a subprimitive function, the compiler emits code that fetches each argument from its location and places the value in a register, and then emits the single CPU instruction that the subprimitive consists of. It arranges for the result of the CPU instruction to be placed in the target location. What I’m describing here is basically what is known as a “compiler intrinsic” in other languages.
If you have a tree of compound expressions, each of which is one of these CPU subprimitive functions, the compiler will assign locations to all the intermediate computed values, determine an order in which to compile the primitives (linearized left to right), and then emit linear code that carries out the primitive operations. If all your code consists of calls to CPU primitives, then the compiler can generate straight-line assembly code CPU instructions. The compiler in this case only acts as a register allocator. From here, you can bootstrap yourself to a set of primitive procedures each of which is written in straght-line assembly code.
When writing an OS, you occasionally run into places where you want
a very specific sequence of CPU instructions. You have a couple of
options: some Lisp compilers offer a way to insert literal assembly
code instructions in the compiled code instruction stream in
a progn
like manner. Naturally such code is
“unsafe”, but it nicely solves the problem where you
need hand-crafted assembly code in your solution. The other
solution is to write the code as a compound expression of CPU
primitives and let the compiler sort out the register allocation.
Joe Marshall — Bloom Filter
@2025-04-11 07:00 · 23 days agoThis is the response time of the Google Mini search appliance on several thousand queries. There is an odd spike at 300 ms. A number of machines were taking exactly 300 ms to respond regardless of the query.
I soon tracked this down to the spell server. This is the microservice that puts “Did you mean foobar?” on the top of the page when you spell something wrong. The results page generator will wait up to 300 ms for the spell server to come up with its suggestions and then it will proceed without it. What appeared to be happening is that the spell server was giving a “no go” signal to the page generator at the beginning of page composition. The results page generator would then wait for the spell server to make a suggestion. The spell server would invariably take too long, so after 300 ms, the results page generator would time out and ship the page as is. This happened often enough that it showed up as a blip in the performance graph.
The spell server was based on a Bloom filter. A Bloom filter is a variation on a hash table where you only record that a bucket exists, but not its contents. A Bloom filter will quickly and reliably tell you if an entry is not in the table, but only probabilistically tell you if an entry is in the table. Bloom filters rely on having a good hash function and having a mostly empty hash table. If the hash table is mostly empty, the Bloom filter will usually end up hitting an empty bucket and returning false immediately.
A quick look at the spellserver showed that the Bloom filter was almost always getting a hit, this would send the “no go” signal and trigger a slow lookup to find the misspelled word. The problem was that the Bloom filter was too small. It had too few buckets, so most buckets had an entry. Words would always get a hit in the Bloom filter and so the search appliance thought that all words were misspelled.
I adusted the size of the Bloom filter, giving it several million buckets. Now correctly spelled words would likely hit an empty bucket and the filter would give a “go” signal to the response page generator. Problem solved, and the spike at 300 ms went away.
John Jacobsen — Lisp Testing Fun
@2025-04-11 00:00 · 23 days ago
I've been making some progress with steelcut
, my Common Lisp
template generator (does every Lisper write one of these as a rite of
passage?), and I keep running into some interesting problems which
have been fun to solve.
Whenever my code starts to accumulate any sort of complexity, I start trying to err on the side of writing more tests, even for "recreational" projects such as this one. The tests help make changes safely, as well as documenting the expected functionality. But test code can be fairly repetitive, often with many small variations and lots of boilerplate, in the midst of which it can be hard to see the forest for the trees.
Such was the case for steelcut
tests. I found myself remembering
Paul Graham's line, "Any other regularity in the code is a sign, to me
at least, that I'm using abstractions that aren't powerful enough -
often that I'm generating by hand the expansions of some macro that I
need to write."
In my current non-Lisp programming work, I most often miss macros when
writing test code. But in this case, the macro I (apparently) needed
to write is here. Very roughly speaking, with-setup
creates a
temporary directory, runs the template generator with a given list of
requested features, and allows the user to check properties of the
generated files.
The macro grew and shrank as I DRYed out and simplified the test code. I realized many of my tests were checking the generated dependencies for the projects (from the .ASD file), so it would be helpful if the macro would make those available to the test code in the form of a function the user could name.
This wound up being a sweet bit of sugar for my tests, but not every
test needed such a helper function. Stubborn "unused variable"
warnings ensued, for those tests which don't use the deps
symbol
bound by the macro. I went back and forth with ChatGPT on this one
(it made several wrong suggestions) until I realized I could give the
variable an explicit _
name and change the behavior based on the
name. This is something I've seen in other languages and was tickled
I could get it so easily here.
I find the resulting tests pretty easy to read:
(test needed-files-created
(with-setup appname "testingapp" appdir _ +default-features+
(loop for file in '("Makefile"
"Dockerfile"
".github/workflows/build.yml"
"build.sh"
"test.sh"
"src/main.lisp"
"src/package.lisp"
"test/test.lisp"
"test/package.lisp")
do
(is (uiop:file-exists-p (join/ appdir file))))))
(test cmd-feature-adds-dependency-and-example
(with-setup appname "testingapp" appdir deps +default-features+
(let ((main-text (slurp (join/ appdir "src/main.lisp"))))
;; :cmd is not there:
(is (not (find :cmd (deps))))
;; It doesn't contain the example function:
(is (not (has-cmd-example-p main-text)))))
;; Add :cmd to features, should get the new dependency:
(with-setup appname "test2" appdir deps (cons :cmd +default-features+)
(let ((main-text (slurp (join/ appdir "src/main.lisp"))))
;; :cmd is now there:
(is (find :cmd (deps)))
;; It contains the example function:
(is (has-cmd-example-p main-text)))))
N.B.: A keyword argument would also work, obviously. For a macro in production code, I would probably go that route instead.
Fun
I've also been thinking about a recent post on Hacker News, and the ensuing discussion, in which both the author and commenters point out that programming Lisp is fun. While I must acknowledge that Lisp has been a life-long attraction/distraction/diversion, I'm noticing that the way it is fun for me is evolving somewhat.
First, I used to find Common Lisp pretty painful because of all its sharp edges - the language is so powerful and flexible, you can shoot yourself in the foot pretty much any way you like, and it's not as easy as some newer languages to get started with. But now, with LLMs, it's much easier to get help when you get into the weeds. Sure, "the corpus of training data was smaller for Lisp than for mainstream languages," blah, blah. But often even the wrong answers from ChatGPT point me in the right direction (some of this is probably just rubberducking). When one's own ignorance is less of an obstacle, the pointy bits become less intimidating.
Second, there are some really good, and fun, books on Lisp. PAIP, Let Over Lambda, Practical Common Lisp, Land of Lisp (plus video!). Lisp has some pretty mind-bending things in it, and I'm enjoying taking the time to dig into some of these books again, understanding things better than the first time I swung by.
Third, I'm regularly stunned by how fast Lisp is. All the tests I've been writing run effectively instantly at the REPL on my M1 Macbook Air. And now that I'm starting to get good enough at the language that it is getting out of my way, that speed is more noticeable, and, well, fun.
Finally, I am intrigued by the history of computing, of which Lisp has been an important part. This is worth saying more about in a future post, but for the moment I find a stable, highly-performant, fun language that has intrigued people for decades to be more of interest than the bleeding edge. (I'm not hating on Rust. Rust is nice. But I don't find it fun, at least not yet.)
Beyond fun, I'm also enjoying "escaping the hamster wheel of backwards incompatibility" for awhile. While so many things around us crumble and whirl, and new forms of AI scare the pants out of us as much as they intrigue, older tools and traditions start to feel more like comfortable tools that weigh down one's hands satisfyingly and invite calm creation.
Joe Marshall — Why I Program in Lisp
@2025-04-10 07:00 · 24 days agoLisp is not the most popular language. It never was. Other general purpose languages are more popular and ultimately can do everything that Lisp can (if Church and Turing are correct). They have more libraries and a larger user community than Lisp does. They are more likely to be installed on a machine than Lisp is.
Yet I prefer to program in Lisp. I keep a Lisp REPL open at all times, and I write prototypes and exploratory code in Lisp. Why do I do this? Lisp is easier to remember, has fewer limitations and hoops you have to jump through, has lower “friction” between my thoughts and my program, is easily customizable, and, frankly, more fun.
Lisp's dreaded Cambridge Polish notation is uniform and universal. I don't have to remember whether a form takes curly braces or square brackets or what the operator precedency is or some weird punctuated syntax that was invented for no good reason. It is (operator operands ...) for everything. Nothing to remember. I basically stopped noticing the parenthesis 40 years ago. I can indent how I please.
I program mostly functionally, and Lisp has three features that help out tremendously here. First, if you avoid side effects, it directly supports the substitution model. You can tell Lisp that when it sees this simple form, it can just replace it with that more complex one. Lisp isn't constantly pushing you into thinking imperatively. Second, since the syntax is uniform and doesn't depend on the context, you can refactor and move code around at will. Just move things in balanced parenthesis and you'll pretty much be ok.
Third, in most computer languages, you can abstract a specific value by
replacing it with a variable that names a value. But you can perform a further
abstraction by replacing a variable that names a quantity with a
function that computes a quantity. In functional programming, you often downplay
the distinction between a value and a function that produces that
value. After all, the difference is only one of time spent waiting for the
answer. In Lisp, you can change an expression that denotes an
object into an abtraction that computes an object by simply wrapping
a lambda
around it. It's less of a big deal these
days, but properly working lambda
expressions were only
available in Lisp until recently. Even so, lambda
expressions are generally pretty clumsy in other languages.
Functional programming focuses on functions (go figure!). These are the ideal black box abstraction: values go in, answer comes out. What happens inside? Who knows! Who cares! But you can plug little simple functions together and get bigger more complex functions. There is no limit on doing this. If you can frame your problem as "I have this, I want that", then you can code it as a functional program. It is true that functional programming takes a bit of practice to get used to, but it allows you to build complex systems out of very simple parts. Once you get the hang of it, you start seeing everything as a function. (This isn't a limitation. Church's lambda calculus is a model of computation based on functional composition.)
Lisp lets me try out new ideas as quickly as I can come up with them. New programs are indistinguishable from those built in to the language, so I can build upon them just as easily. Lisp's debugger means I don't have to stop everything and restart the world from scratch every time something goes wrong. Lisp's safe memory model means that bugs don't trash my workspace as I explore the problem.
The REPL in lisp evaluates expressions, which are the fundamental fragments of Lisp programs. You can type in part of a Lisp program and see what it does immediately. If it works, you can simply embed the expression in a larger program. Your program takes shape in real time as you explore the problem.
Lisp's dynamic typing gives you virtually automatic ad hoc polymorphism. If you write a program that calls +, it will work on any pair of objects that have a well-defined + operator. Now this can be a problem if you are cavalier about your types, but if you exercise a little discipline (like not defining + on combinations of strings and numbers, for example), and if you avoid automatic type coercion, then you can write very generic code that works on a superset of your data types. (Dynamic typing is a two-edged sword. It allows for fast prototyping, but it can hide bugs that would be caught at compile time in a statically typed language.)
Other languages may share some of these features, but Lisp has them all together. It is a language that was designed to be used as a tool for thinking about problems, and that is the fun part of programming.
Joe Marshall — Lisp Programs Don't Have Parentheses
@2025-04-09 11:46 · 25 days agoLisp programs don't have parentheses — they are made of nested linked lists. The parentheses only exist in the printed representation — the ASCII serialization — of a Lisp program. They tell the Lisp reader where the nested lists begin and end. Parenthesis are the contour lines in the topographic map of your Lisp program.
The parentheses allow other programs, such as editors, to manipulate the program text as a tree structure. You can use the parenthesis to determine the structure of the Lisp program without needing a complicated parser. You only need to know how to count the opening and closing parentheses to determine the boundaries of expressions in the text of a program. You don't need an IDE or a treesitter process running a language parser in the background to make sense of the lexical structure of the program.
This makes refactoring of a Lisp program easy because the expression boundaries are explicitly marked. Additionally, Lisp expressions do not have a dependency on the context in which they appear — Lisp expressions don't change form when you move them around. Contrast this with, for example, expression sequences in C. In C, in a statement context, expressions are terminated by semicolons, but in an expression context, they are separated by commas. If you move a sequence of expressions in C from statement to expression context, you also have to change the semicolons to commas.
As another example, in C and Java, conditional statements follow
the if … else …
form, but
conditional expressions use the infix ternary ?:
operator, so moving conditionals around may require a substantial
edit. In a language without ternary conditionals, like Go and
Rust, wrapping a subexpression with a conditional may require
large rewrites of the surrounding code.
These days, people use complex IDEs to write and refactor code. But still, you are dependent upon what the IDE provides. A refactoring that is not supported by the IDE has to be done manually. But Lisp just lets you move expressions around as text. No muss, no fuss. You can even use a simple text editor to do it.
Joe Marshall — Are You Tail Recursive?
@2025-04-07 07:00 · 27 days agoI was trying to write a procedure that would test whether tail recursion was enabled or not. It turns out that this is semi-decidable. You can only tell if it is not tail recursive by blowing the stack, but you can never be sure that somebody didn’t make a really, really big stack.
But for the sake of argument, let’s say assume that 228 recursive calls is enough to blow the stack. Also, let’s assume that the system will correctly signal an error and be able to recover from the blown stack once it is unwound. Then we can test for tail recursion as follows:
(defconstant +stack-test-size+ (expt 2 28)) (defun test-self-tail-recursion (x) (or (> x +stack-test-size+) (test-self-tail-recursion (1+ x)))) (defun test-mutual-tail-recursion (x) (or (> x +stack-test-size+) (test-mutual-tail-recursion-1 (1+ x)))) (defun test-mutual-tail-recursion-1 (x) (or (> x +stack-test-size+) (test-mutual-tail-recursion (1+ x)))) (defun self-tail-recursive? () (ignore-errors (test-self-tail-recursion 0))) (defun mutually-tail-recusive? () (ignore-errors (test-mutual-tail-recursion 0)))
Naturally, your optimize
settings are going to affect
whether or not you have tail recursion enabled, and even if this
code says it is enabled, it doesn’t mean that a different
compilation unit compiled at a different time with different
optimizations would be tail recursive as well. But if you run this
in your default environment and it returns NIL, then you can be
pretty sure your default is not tail recursive. If it
returns T, however, then there are at least some conditions under
which your system is tail recursive.
Use of certain features of Common Lisp will “break”
tail recursion, for example special
variable binding, multiple-value-bind
, unwind-protect
,
and catch
, and basically anything that marks the stack
or dynamically allocates on the stack. If control has to return to
the current function after a call, then it is not tail recursive,
but if control can return directly to the caller of the current
function, then the compiler should be able to make a tail recursive
call.
Most, but not all, Common Lisp implementations can be configured to
enable tail recursion, and it is usually possible to disable it if
desired. If you want tail recursion, but your implementation cannot
provide it, you are SOL. If you don’t want tail recursion, but your
implementation provides it by default, there are a number of things
you can do to disable it. Usually, you can set the debugging
options to retain every stack frame, or you can set the debug
optimization to disable tail recursion. If the recursive call is
not in tail position, then it is incorrect to tail call it, so you
can disable tail recursion by moving the recursive call out of tail
position. For example, you could write
a without-tail-call
macro:
(defmacro without-tail-call (form) (let ((value (gensym))) `((lambda (,value) ,value) ,form))) ;; Use like this: (defun factorial (x &optional (acc 1)) (if (zerop x) (progn (cerror "Return the factorial" "Computed a factorial") acc) (without-tail-call (factorial (1- x) (* acc x)))))
Running this program will drop us into the debugger because of
the cerror
and we can inspect the stack.
> (factorial 100) Computed a factorial [Condition of type SIMPLE-ERROR] Restarts: 0: [CONTINUE] Return the factorial 1: [RETRY] Retry SLY mREPL evaluation request. 2: [*ABORT] Return to SLY’s top level. 3: [ABORT] abort thread (#<THREAD tid=179 "sly-channel-1-mrepl-remote-1" RUNNING {1000BA8003}>) Backtrace: 0: (FACT 0 2432902008176640000) 1: (FACT 1 2432902008176640000) 2: (FACT 2 1216451004088320000) 3: (FACT 3 405483668029440000) 4: (FACT 4 101370917007360000) 5: (FACT 5 20274183401472000) 6: (FACT 6 3379030566912000) ...
As requested, each recursive calls pushes a stack frame.
But what if the compiler “optimizes” ((λ
(x) x) (foo))
to simply be (foo)
? That is a
questionable “optimization”. Lisp is a call-by-value
language, meaning that the arguments to a function are fully
evaluated before the function is applied to them. The call
to (foo)
should be fully evaluated before
applying (λ (x) x)
to it. The
“optimization” essentially treats (foo)
as unevaluted
and then tail calls it after applying the lambda expression. This
is call-by-name evaluation. While anything is possible, an
“optimization” that sometimes changes the function
call semantics from call-by-value to call-by-need is dubious.
Joe Marshall — When Laymen Try to be Programmers
@2025-04-06 08:23 · 28 days agoWhen I worked on Google Offers (a Groupon clone), we had a user interaction problem. When a user purchased an item, there were three important steps that we wanted to acknowledge:
- When Google recieves an order. The user should get positive feedback that the order was recognized. If the user doesn’t get this, they will either re-submit the order, or be surprised when the order is fulfilled. The order is placed on a queue.
- When Google processes an order from the queue. The user should get positive feedback that Google is working on fulfilling the order. Usually, an order is fulfilled reasonably quickly, but there are situations where critical systems are unavailable and the order has to remain in the queue for an extended period of time (several hours). The user gets nervous if they get ghosted by the system after getting positive feedback that Google got the order.
- Finally, when Google has made the decision whether to fulfill the order or has declined the order. The user needs to be told whether to expect shipment or a refund. If a refund, then the user can take action to re-submit the order.
So in submitting an order to Google Offers, a total of three emails would be sent to the user so they could watch their order proceed through the system. The sending of these emails was controlled by the “commerce” API. The commerce API was a walled off section of the Google infrastructure that knew how to talk to the credit card companies and charge money. Normal Google code was not allowed to do these things but had to work via the commerce API, and the commerce API would take care of ensuring that the appropriate pop-ups would appear on the user’s screen and that the credit card information was securely obtained. Normal Google code never got its hands on the user’s credit card information, it only got a "go/no-go" signal from the commerce API.
So the commerce API would be the system actually taking the steps of recieving the order, charging the credit card, and returning the go/no-go signal to our system. We would instruct it to send email for each of these steps. So far so good.
The problem was that often the order would go through very quickly. The emails were processed in batches, so the email that acknowledged the reciept of the order could end up being received after the email that acknowledged that the order had been fulfilled. The user would first get an email saying "We charged your card." and only after this would they get an email saying "We got your order." This would confuse the user.
There was no way to add an artifical time lag, nor would we want to. We could not guarantee that the emails would arrive in the right order. (Even if we could, we couldn’t guarantee that the user would read them in the right order.) The solution that I proposed was to explicitly number the emails so that each email would say "This is email number N of 3 expected emails." and even perhaps a small message that said "Emails can arrive in the wrong order." If a user got email 2 first, then email 1, they could figure it out.
But the product managers didn’t see it that way. As far as they were concerned, it was confusing when email 1 arrived after email 2, so they wanted to not send email 1 if email 2 had been recieved. This is a nice idea, but I pointed out that we had no control over the order of arrival of emails, so there was no way to know if email 2 would be received prior to email 1 at the time we were sending email 1. They were adamant: "Don’t send the second email (that is, email 1, which arrived second in some situations)."
Ok, then. I adjusted the code to suppress the sending of email 1. This solved the problem of email 1 arriving after email 2, sure, but recall that email 1 was the "Google has received your order and has placed it on a queue to be processed" acknowledgement. Now when people placed an order, they would no longer get confirmation that Google had received it. Usually, Google would process the order in a timely manner and they’d quickly get email 2 which said "We are processing your order", but if there were some reason that we could not process the queue for some time, the user would be left in the dark about whether Google had heard them in the first place.
Complaints poured in.
The subsequent conversation was something like this:
“Why aren’t you acknowledging that the order has been received?”
“You explicitly told us to not send that email. See this communication (cite reference).”
“But that was not what we meant. We meant, don’t send it if the user has received the ‘Your order has been processed.’ email.”
“How are we supposed to know if email system delivered his mail and that he read it in the right order? We’re good, but not that good.”
“You mean emails can arrive or be read in the wrong order?”
“Exactly what we said in this communication (cite reference).”
“...”
“May I suggest we number the emails so that the user can figure it out if they arrive in the wrong order?”
“No, don’t do that. Just put it back the way it was.”
Done, and we are back to the original problem.
Out-of-order packets are an old problem that existed and was solved before computers. Computer programmers are, of course, very aware of the problem and the solutions. Computer programmers are well versed in the problems of “process”, and when laymen try to play at being programmers by interfering with process, they generally screw it up.
Joe Marshall — Suddenly
@2025-04-04 18:18 · 29 days agoJoe Marshall — Blacksmithing and Lisp
@2025-04-03 07:00 · 31 days agoOne of my hobbies is blacksmithing (not farrier work). Mild steel is an amazingly versatile material. It's very strong and hard at room temperature and it gets soft and easily workable when you heat it up. You use a hammer to move the metal once it is hot. You don't have to hit it very hard, just firmly. Hot metal is a very forgiving medium. If you make a mistake, simply heat the work up and try again. You rarely encounter mistakes you cannot recover from and you have to throw your work away.
A blacksmith uses tongs to manipulate work that would otherwise be too hot to handle. You don't want to drop a piece of hot steel, so you'd like your tongs to be a good fit on your work. Find some tongs that are an approximate fit and stick them in the fire to get good and hot. When they have softened, put the hot tongs around the cold workpiece and tap them into shape with your hammer. Voila! Custom tongs.
When I first saw this trick I was quite amused. It reminded me of Lisp programming — you can work on your problem, or you can customize the language to fit your problem better. And, yes, I'm the kind of nerd that sees a blacksmith trick and thinks “Lisp!”
Another computer-sciency question is one of bootstrapping. Where do tongs come from? How do you make tongs if you don't have them? This isn't too hard. A simple pair of tongs is basically two sticks with a rivet. You can shape half a pair of tongs (a single tong) by holding one end while you work the other. It will take a bit of time for the end in your hand becomes too hot to hold.
A good part of blacksmithing is creating ad hoc tools in pursuit of the end goal. You fairly often recur and create tools that help create tools. Metacircular blacksmithing.
The downside of working hot steel is that it is quite hot. You will get burned, but usually only mildly. Your reflexes take over pretty quickly when you touch hot metal. Then you learn early on that if you drop something, you do not attempt to catch it.
Joe Marshall — Lisp at Work
@2025-04-02 16:09 · 31 days agoLisp is not a good fit for most of the projects I'm doing at work. Our company has few Lisp programmers, so there would be no one to help maintain any code I wrote, and no one has a Lisp environment set up on their machine. I'll sometimes do a quick prototype in Lisp, and I keep a REPL open on my machine, but I don't develop code I expect to share or deploy. Once I have a prototype or proof of concept, I'll write the real thing in a language that is more commonly used at work.
But sometimes, Lisp is such a good fit for a problem that it overrides the considerations of how well it fits into your company's ecosystem. Not often, but sometimes.
I had to migrate some builds from CircleCI 2 to CircleCI 4. We had tried (twice) and failed (twice) to do an in-place upgrade of the server, so we instead brought up CircleCI 4 in parallel and migrated the builds over. To migrate a build, we had to extract the build information from the old server and import it into the new server. The API to the old CircleCI server did not expose all the data we needed, and there was no API on the new server that let you import everything we needed.
But CircleCI is written in clojure. So you can connect to a running instance of CircleCI and open a REPL. The REPL has access to all the internal data structures. It is an easy matter to write some lisp code to extract the necessary data and print it to stdout. It is also an easy matter to write some lisp code to read some data from stdin and import it into the new server.
The migration code could be written in any language, but it had to talk to a clojure REPL, format data in such a way that the clojure REPL could read it, and parse the output from the clojure REPL, which was going to be a Lisp object. Any language with a Common Lisp reader and printer would do, but you sort of get these for free if you actually use Lisp. I knew that the migration code could be written in Lisp because I had already prototyped it.
So I wrote a migration function that would take the name of the project to be migrated. The I created a Dockerfile that would boot an instance of sbcl on a core file. I preloaded the migration code and dumped the core. I ran the Dockerfile and got an image that, when run, would migrate a single project read from the command line. I then created a server that a user could visit, enter the name of his project, and it would run the Docker image as a kubernetes job.
We migrated most of the projects this way. At one point, I wrote an additional script that would read the entire list of projects in the old server and simply print them to stdout. After we shut down the migration server, I'd get requests from people that didn't seem to understand what a deadline was. I could prime the migration script from the file and it would initialize a project on the new server with the old state that I dumped. Migration stragglers could often recover this way.
Using Lisp for the migration tool did not add risk to the company. Just using the tool did not require Lisp knowledge. Anyone that needed to understand the migration process in detail had to understand clojure anyway. The migration took place over a period of weeks, and the migration tool was shut down at the end of this period. Long term maintenance was not a concern. Users of the migration tool did not have to understand Lisp, or even know what language was being used. It was a black box kubernetes job. I got buy-in from management because the tool simply worked and solved a problem that had twice been been unsuccessfully attempted before.
Joe Marshall — Vibe Coding, final word
@2025-04-01 07:00 · 33 days agoI couldn't leave it alone. This AI was going to write some Lisp code if I had to force it. This isn't “vibing” anymore. We're going to be pecise, exact, and complete in our instructions, and we're going to check the results.
Again, I'm taking on a Minesweeper clone as the problem. All the code was to be written in a single file using a single package. The AI simply didn't understand the problem of forward references to symbols in other packages. Perhaps a game loop is beyond the ability of the AI. I wrote a basic game loop that initializes all the required libraries in correct order with unwind-protects to clean up in reverse order. I wrote a main function that creates a window and a renderer to draw on it, and a game loop that polls for events and handles keypresses and the quit event. This is a basic black window that has no behavior beyond the ability to quit. There should be no need for the AI to modify this code.
The AI used the GPT-4o model. Instructions were given in precise, imperative English. For example,
“Each cell on the board is in one of these states: hidden, flagging, flagged, unflagging, exposing, exposed Cells start out in hidden state. When a cell is hidden, it renders as a blank square. When a cell is hidden and the mouse is over the cell and the right button is down, the cell enteres the flagging state. When a cell is flagging and the mouse is over the cell and the right button is up, the cell enters the flagged mode. When a cell is flagged and the mouse is over the cell and the right button is down, the cell enters unflagging. When the cell is unflagging, the mouse is over the cell and and right button is up, the cell enters hidden. Cells that are flagging or flagged display as the flag texture. Cells that are hidden or unflagging display as the blank texture.”
This is programming, not vibing. There is always room for misunderstanding, but I spelled out the details of part of the state transitions that I wanted the AI to implement. In particular, notice that when flagging a cell, there are hidden states beyond the flagged and unflagged states. These are necessary to make the effect of flagging and unflagging be edge triggered. I didn't trust the AI to know about this, so I spelled it out.
Sometimes I could write simple directions, such as:
“When rendering a cell, if it is under the mouse, highlight the cell with a red border.”
Or:
“When the cell is in the exposing state, it should display as a blank, depressed cell.”
But then again,
“When the mouse is over a cell, if the cell is in hidden state and left mouse button is down, the cell enters the exposing state. If a cell is in exposing state and the mouse is not over the cell, the cell reverts to hidden state. If a cell is in exposing state, and the mouse is over the cell, and the left mouse button is raised, the cell enters the exposed state.”
The AI did a surprisingly good job at handling the parenthesis. There was one function that it kept adding a stray parenthesis to. I went back more than once to delete that extra parenthesis when the reader barfed.
At one point, the AI accidentally removed the abiity to place a flag on a cell. I asked it to restore that ability, but I didn't give instructions in as great detail as I did before. It added transitions to and from the flagged state, but it didn't add the intermediate flagging and unflagging states as I had explicitly directed it to before. As a result, flagging ended up being non-deterministic, with the flag being toggled on and off every 16 ms. I had to go back and add the intermediate states by hand again to restore the determinism.
After a few hours of this, I had a working Minesweeper clone. Let's look at the generated code.
;; -*- mode: lisp; package: "VIBE" -*- (in-package "VIBE") ;; Define constants for magic numbers (defparameter *tile-size* 32) ;; Size of each tile in pixels (defparameter *game-width* 25) ;; Number of columns (defparameter *game-height* 15) ;; Number of rows (defparameter *frame-delay* 16) ;; Delay between frames in milliseconds ;; Define cell states (defconstant +hidden+ 'hidden) (defconstant +flagging+ 'flagging) (defconstant +flagged+ 'flagged) (defconstant +unflagging+ 'unflagging) (defconstant +exposing+ 'exposing) (defconstant +exposed+ 'exposed) ;; Define game states (defconstant +playing+ 'playing) (defconstant +won+ 'won) (defconstant +lost+ 'lost)
It is a little odd to define the state constants as symbols. This adds a pointless extra level of symbolic indirection. I would have used keywords instead of symbols.
;; Global game state (defparameter *game-state* +playing+) ;; The game board (defparameter *board* nil) ;; The game board content (e.g., :mine or a number) (defparameter *board-content* nil) (defun initialize-board () "Initialize the game board with all cells in the hidden state and place mines." ;; Initialize the board state (setf *board* (make-array (list *game-height* *game-width*) :initial-element +hidden+)) ;; Initialize the board content (setf *board-content* (make-array (list *game-height* *game-width*) :initial-element 0)) ;; Place 50 mines randomly (loop repeat 50 do (let ((row (random *game-height*)) (col (random *game-width*))) ;; Ensure no duplicate mines (unless (eql (aref *board-content* row col) :mine) (setf (aref *board-content* row col) :mine) ;; Update adjacent cells (update-adjacent-cells row col))))) (defun update-adjacent-cells (row col) "Increment the mine count for all adjacent cells." (loop for r from (max 0 (1- row)) to (min (1+ row) (1- *game-height*)) do (loop for c from (max 0 (1- col)) to (min (1+ col) (1- *game-width*)) do (unless (eql (aref *board-content* r c) :mine) (incf (aref *board-content* r c))))))
You can tell that I didn't write this code. I hate loop.
The textures are loaded from PNG files just before beginning the game loop. The AI decided to maintain them as four separate values that are passed around to the functions that need them. This will get out of hand quickly. A table of resources would be better.
(defun render-frame (renderer blank-texture flag-texture mine-texture digit-textures) (sdl2:set-render-draw-color renderer 0 0 0 0) (sdl2:render-clear renderer) ;; Render game state (loop for row from 0 below *game-height* do (loop for col from 0 below *game-width* do (render-tile renderer row col blank-texture flag-texture mine-texture digit-textures)))) (defun render-tile (renderer row col blank-texture flag-texture mine-texture digit-textures) "Render a single tile based on its state and highlight it if under the mouse." (let ((state (aref *board* row col)) (content (aref *board-content* row col)) ;; Content of the cell (e.g., :mine or a number) (dest-rect (sdl2:make-rect (* col *tile-size*) (* row *tile-size*) *tile-size* *tile-size*))) ;; Render the tile based on its state (cond ;; Hidden or unflagging state: render blank texture ((or (eql state +hidden+) (eql state +unflagging+)) (sdl2:render-copy renderer blank-texture :dest-rect dest-rect)) ;; Flagging or flagged state: render flag texture ((or (eql state +flagging+) (eql state +flagged+)) (sdl2:render-copy renderer flag-texture :dest-rect dest-rect)) ;; Exposing state: render blank texture with a depressed effect ((eql state +exposing+) (sdl2:render-copy renderer blank-texture :dest-rect dest-rect) (sdl2:set-render-draw-color renderer 50 50 50 100) ;; Semi-transparent dark overlay (sdl2:render-fill-rect renderer dest-rect)) ;; Exposed state: render mine or digit ((eql state +exposed+) (cond ;; If the cell contains a mine, render the mine texture ((eql content :mine) (sdl2:render-copy renderer mine-texture :dest-rect dest-rect)) ;; If the cell contains a digit (positive number), render the corresponding digit texture ((and (integerp content) (> content 0)) (let ((digit-texture (gethash content digit-textures))) (when digit-texture (sdl2:render-copy renderer digit-texture :dest-rect dest-rect)))) ;; Otherwise, render a default exposed tile (e.g., light gray) (t (sdl2:set-render-draw-color renderer 200 200 200 255) ;; Light gray for exposed (sdl2:render-fill-rect renderer dest-rect))))) ;; Highlight the tile with a red border if it is under the mouse (multiple-value-bind (mouse-x mouse-y _) (sdl2:mouse-state) (declare (ignore _)) (let ((mouse-row (floor (/ mouse-y *tile-size*))) (mouse-col (floor (/ mouse-x *tile-size*)))) (when (and (= row mouse-row) (= col mouse-col)) (sdl2:set-render-draw-color renderer 255 0 0 255) ;; Red color (sdl2:render-draw-rect renderer dest-rect) (sdl2:set-render-draw-color renderer 0 0 0 255)))))) ;; Reset color to black
Each time we go around the game loop, we update the state the cell under the mouse. The AI decided to pass the state input as spread arguments to the update function.
(defun update-cell-state (row col mouse-over left-button-down left-button-up right-button-down right-button-up) "Update the state of a cell based on mouse input." (let ((state (aref *board* row col))) (cond ;; If the cell is hidden and the left mouse button is down, it enters the exposing state ((and (eql state +hidden+) mouse-over left-button-down) (setf (aref *board* row col) +exposing+)) ;; If the cell is exposing and the mouse is not over it, it reverts to hidden ((and (eql state +exposing+) (not mouse-over)) (setf (aref *board* row col) +hidden+)) ;; If the cell is exposing, the mouse is over it, and the left mouse button is raised, it enters the exposed state ((and (eql state +exposing+) mouse-over left-button-up) (setf (aref *board* row col) +exposed+) ;; If the cell contains a mine, the game is lost and all mines are exposed (when (eql (aref *board-content* row col) :mine) (setf *game-state* +lost+) (expose-all-mines)) ;; If the cell has zero neighboring mines, recursively expose neighbors (when (and (integerp (aref *board-content* row col)) (= (aref *board-content* row col) 0)) (expose-neighbors row col))) ;; If the cell is hidden and the right mouse button is down, it enters the flagging state ((and (eql state +hidden+) mouse-over right-button-down) (setf (aref *board* row col) +flagging+)) ;; If the cell is flagging and the right mouse button is up, it enters the flagged state ((and (eql state +flagging+) mouse-over right-button-up) (setf (aref *board* row col) +flagged+)) ;; If the cell is flagged and the right mouse button is down, it removes the flag ((and (eql state +flagged+) mouse-over right-button-down) (setf (aref *board* row col) +unflagging+)) ((and (eql state +unflagging+) mouse-over right-button-up) (setf (aref *board* row col) +hidden+))))) (defun poll-mouse-and-update () "Poll the mouse position and button states, and update the board accordingly." (when (eql *game-state* +playing+) ;; Only process mouse input if the game is playing (multiple-value-bind (x y buttons) (sdl2:mouse-state) (let ((row (floor (/ y *tile-size*))) (col (floor (/ x *tile-size*))) (left-button-down (logbitp 0 buttons)) ;; SDL_BUTTON_LEFT is bit 0 (right-button-down (logbitp 2 buttons))) ;; SDL_BUTTON_RIGHT is bit 2 (when (and (>= row 0) (< row *game-height*) (>= col 0) (< col *game-width*)) ;; Update the cell state based on mouse input (update-cell-state row col t ;; mouse-over is true for the current cell left-button-down (not left-button-down) right-button-down (not right-button-down)))))))
This illustrates that while the lights appear to be on, no one is
at home. The mouse-over
variable is always true, there
is no need for it to exist at all. There is no need to
pass both left-button-down
and its complement. Same
with right-button-down
.
I did allow the AI to modify game-loop
, but the
modifications were subject to careful scrutiny to make sure that the
game would continue to run. In particular, one time it wanted to
add handlers for mouse events. I told it no, and that it could poll
the mouse state as necessary instead.
(defun game-loop (window renderer blank-texture flag-texture mine-texture digit-textures game-over-texture) "Main game loop." (declare (ignore window)) ;; Main game loop (sdl2:with-event-loop (:method :poll) (:idle () ;; Clear the screen (sdl2:set-render-draw-color renderer 0 0 0 255) ;; Black background (sdl2:render-clear renderer) ;; Poll mouse and update game state (poll-mouse-and-update) ;; Render the game frame (render-frame renderer blank-texture flag-texture mine-texture digit-textures) ;; Render the "Game Over" overlay if the game is lost (when (eql *game-state* +lost+) (let ((screen-width (* *tile-size* *game-width*)) (screen-height (* *tile-size* *game-height*))) ;; Set blend mode and alpha for transparency (sdl2:set-texture-blend-mode game-over-texture :blend) (sdl2:set-texture-alpha-mod game-over-texture 192) ;; 75% transparency ;; Render the texture as a full-screen overlay (let ((dest-rect (sdl2:make-rect 0 0 screen-width screen-height))) (sdl2:render-copy renderer game-over-texture :dest-rect dest-rect)))) ;; Present the rendered frame (sdl2:render-present renderer) ;; Delay for the next frame (sdl2:delay *frame-delay*)) (:keydown (:keysym keysym) (cond ;; Reset the game when the 'o' key is pressed ((eql (sdl2:scancode keysym) :scancode-o) (reset-game)) ;; Quit the game when the 'x' key is pressed ((eql (sdl2:scancode keysym) :scancode-x) (sdl2:push-quit-event)) ;; Lose the game and expose all mines when the 'p' key is pressed ((eql (sdl2:scancode keysym) :scancode-p) (progn (setf *game-state* +lost+) (expose-all-mines))))) (:quit () t)))
Notice that in this game loop, we're not accounting for the time it takes to update the game state and render the frame. If this game really tried to animate anything, the animation would be jittery. A better game loop would track real time and refresh accordingly.
For a simple game such as this, it makes sense to load the all the bitmaps into memory at the get-go. For a more complicated game with many levels, you might not be able to fit them all in memory.
Passing the surfaces around as arguments is not going to work when you have a lot of them.
(defun initialize () "Initialize the game, load textures, and create the game board." (initialize-board) ;; Initialize the game board (let ((blank-surface nil) (flag-surface nil) (mine-surface nil) (game-over-surface nil) (digit-surfaces (make-hash-table))) (unwind-protect (progn ;; Load PNG surfaces (setq blank-surface (sdl2-image:load-image (asdf:system-relative-pathname "vibe" "textures/blank.png"))) (setq flag-surface (sdl2-image:load-image (asdf:system-relative-pathname "vibe" "textures/flag.png"))) (setq mine-surface (sdl2-image:load-image (asdf:system-relative-pathname "vibe" "textures/mine.png"))) ;; Load digit textures (e.g., "1.png", "2.png", etc.) (loop for i from 1 to 8 do (setf (gethash i digit-surfaces) (sdl2-image:load-image (asdf:system-relative-pathname "vibe" (format nil "textures/~a.png" i))))) ;; Create the "GAME OVER" surface (setq game-over-surface (create-game-over-surface)) ;; Create the window and renderer (sdl2:with-window (window :title "Vibe" :x 0 :y 0 :w (* *tile-size* *game-width*) :h (* *tile-size* *game-height*) :flags '(:shown)) (sdl2:with-renderer (renderer window :index -1 :flags '(:accelerated)) (let ((blank-texture (sdl2:create-texture-from-surface renderer blank-surface)) (flag-texture (sdl2:create-texture-from-surface renderer flag-surface)) (mine-texture (sdl2:create-texture-from-surface renderer mine-surface)) (digit-textures (make-hash-table)) (game-over-texture (sdl2:create-texture-from-surface renderer game-over-surface))) ;; Convert digit surfaces to textures (maphash (lambda (key surface) (setf (gethash key digit-textures) (sdl2:create-texture-from-surface renderer surface))) digit-surfaces) (unwind-protect (game-loop window renderer blank-texture flag-texture mine-texture digit-textures game-over-texture) ;; Cleanup textures (sdl2:destroy-texture blank-texture) (sdl2:destroy-texture flag-texture) (sdl2:destroy-texture mine-texture) (sdl2:destroy-texture game-over-texture) (maphash (lambda (_key texture) (declare (ignore _key)) (sdl2:destroy-texture texture)) digit-textures))))))) ;; Cleanup surfaces (when flag-surface (sdl2:free-surface flag-surface)) (when blank-surface (sdl2:free-surface blank-surface)) (when mine-surface (sdl2:free-surface mine-surface)) (when game-over-surface (sdl2:free-surface game-over-surface)) (maphash (lambda (_key surface) (declare (ignore _key)) (sdl2:free-surface surface)) digit-surfaces)))
In Minesweeper, if you click on a cell with no neighboring mines, all the neighboring cells are exposed. This will open up larger areas of the board. The AI did a good job of implementing this, but I was careful to specify that only the hidden cells should be exposed. Otherwise, the recursion would not bottom out because every cell is a neighbor of its neighbors.
(defun expose-neighbors (row col) "Recursively expose all hidden neighbors of a cell with zero neighboring mines." (loop for r from (max 0 (1- row)) to (min (1+ row) (1- *game-height*)) do (loop for c from (max 0 (1- col)) to (min (1+ col) (1- *game-width*)) do (when (and (eql (aref *board* r c) +hidden+)) ;; Only expose hidden cells (setf (aref *board* r c) +exposed+) ;; If the neighbor also has zero mines, recursively expose its neighbors (when (and (integerp (aref *board-content* r c)) (= (aref *board-content* r c) 0)) (expose-neighbors r c))))))
We need a way to get the game back to the initial state.
(defun reset-game () "Reset the game by reinitializing the board and setting the game state to playing." (initialize-board) (setf *game-state* +playing+))
The AI writes buggy code. Here is an example. It is trying figure out if the player has won the game. You can state the winning condition in couple of different ways.
- All the cells that are not mines are exposed.
- All the cells that are mines are flagged, all flagged cells contain mines.
This does't quite achieve either of these.
(defun check-win-condition () "Check if the player has won the game." (let ((won t)) ;; Assume the player has won until proven otherwise (loop for row from 0 below *game-height* do (loop for col from 0 below *game-width* do (let ((state (aref *board* row col)) (content (aref *board-content* row col))) (when (and (not (eql state +exposed+)) ;; Cell is not exposed (not (or (eql state +flagged+) ;; Cell is not flagged (eql content :mine)))) ;; Cell does not contain a mine (setf won nil))))) (when won (setf *game-state* +won+))))
create-game-over-surface
prepares a surface with the
words “Game Over” writ large.
(defun create-game-over-surface () "Create a surface for the 'GAME OVER' splash screen using SDL2-TTF." (let ((font nil) (text-surface nil)) (unwind-protect (progn ;; Load the font (adjust the path and size as needed) (setq font (sdl2-ttf:open-font (asdf:system-relative-pathname "vibe" "fonts/arial.ttf") 72)) ;; Render the text "GAME OVER" in red (setq text-surface (sdl2-ttf:render-text-solid font "GAME OVER" 255 0 0 255))) ;; Cleanup (when font (sdl2-ttf:close-font font))) text-surface))
The main
function initializes the SDL2 library and its
auxiliar libraries along with unwind-protect
s to
uninitialize when we leave the game. The AI was not permitted to
change this code.
(defun main () (sdl2:with-init (:video) (unwind-protect (progn (sdl2-image:init '(:png)) (unwind-protect (progn (sdl2-ttf:init) (initialize)) (sdl2-ttf:quit))) (sdl2-image:quit))))
If you step on a mine, it exposes the other mines.
(defun expose-all-mines () "Expose all mines on the board." (loop for row from 0 below *game-height* do (loop for col from 0 below *game-width* do (when (eql (aref *board-content* row col) :mine) (setf (aref *board* row col) +exposed+)))))
Conclusion
This wasn't “vibe coding”. This was plain old coding, but filtered through an English language parser. It added an extra level of complexity. Not only did I have to think about what should be coded, I had to think about how to phrase it such that the AI would generate what I had in mind and not disturb the other code.
Whenever I tried to let go and “vibe”, the AI would generate some unworkable mess. Programming is a craft that requires training and discipline. No dumb pattern matcher (or sophisticated one) is going to replace it.
In languages other that Common Lisp, you might get further. Consider Java. It takes a page and half of boilerplate to specify the simplest first-class object. An AI can easily generate pages and pages of boilerplate and appear to be quite productive. But you've missed the point if you think that it is better to generate boilerplate automatically than to use abstractions to avoid it and a language that doesn't need it.
Neil Munro — Ningle Tutorial 5: Environmental Variables
@2025-03-31 21:30 · 33 days agoContents
- Part 1 (Hello World)
- Part 2 (Basic Templates)
- Part 3 (Introduction to middleware and Static File management)
- Part 4 (Forms)
- Part 5 (Environmental Variables)
- Part 6 (Database Connections)
Introduction
Welcome back, before we begin looking at databases we need to look at storing process related data in the application environment, this month will be a relatively short, but important part in this series.
If you are unfamiliar, there's a methodology called 12factor for building web applications that advocates for storing variable data in environment variables. In anticipation of working with databases that are going to need database names, potentially usernames and passwords etc, we need a system to load this data into our system without writing this potentially sensitive information down in the application code itself.
Environmental Variables are just that, variables defined in the environment of a process. Your operating system defines a number of these, for example, your system will have an area where files might be stored temporarily, and a program may run on different systems, but if both systems have an environmental variable TMP
then the program can read the value of the TMP
environmental variable and use the directory the system specifies, making it portable across systems without needing to change the code. You just read the value defined by the TMP
environmental variable from the system and that's it!
When a process starts, it gets a copy of all system defined environmental variables, although a process generally can't override the values to affect other processes, it is, however, possible to change existing ones or add new ones to the running process, which is what we are going to do here. We have a process we want to run, but want to hide sensitive information in the environment and so will inject new environmental variables into the running process without adding to the system environmental variables for any other process.
Typically we do this by creating a file (usually called .env
) that will define the new values, and this file will be loaded as the program starts, importantly this file will NOT be stored in version control, otherwise we wouldn't hide the data, just move it to a different file. It is very important to ensure that you ignore this file!
In order to use this technique we will be using the cl-dotenv package, so first ensure you have added it to your dependencies in the project asd file.
:depends-on (:clack
:cl-dotenv
:ningle
:djula
:cl-forms
:cl-forms.djula
:cl-forms.ningle)
Integrating the package is quite simple, just below where we create the application object in main.lisp
, we use the package to load in the custom environmental variables.
(defvar *app* (make-instance 'ningle:app))
(dotenv:load-env (asdf:system-relative-pathname :ningle-tutorial-project ".env"))
It is important to ensure we have a .env
file prior to starting the application though! We are likely going to use sqlite (at least in the beginning) so we need to tell our application where to store the database file, for now that will be the only thing we store in the .env
file, we can always add to the file as/when we need to, and this tutorial serves as an introduction to injecting environmental variables, so if it works for one, it'll work for many! Please note, this .env
file must be in the root of your project.
DBPATH=~/quicklisp/local-projects/ningle-tutorial-project/ntp.db
To confirm this works, we will add a format
expression to prove things are as we need them to be, in the start function, we use the uiop package (which comes installed with sbcl) to get the variable.
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(format t "Test: ~A~%" (uiop:getenv "DBPATH"))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder :session
(:static
:root (asdf:system-relative-pathname :ningle-tutorial-project "src/static/")
:path "/public/")
*app*)
:server server
:address address
:port port))
If you start the application now, you should see the value being loaded and printed out.
Test: ~/quicklisp/local-projects/ningle-tutorial-project/ntp.db
NOTICE: Running in debug mode. Debugger will be invoked on errors.
Specify ':debug nil' to turn it off on remote environments.
Woo server is started.
Listening on 127.0.0.1:8000.
#S(CLACK.HANDLER::HANDLER
:SERVER :WOO
:SWANK-PORT NIL
:ACCEPTOR #<BT2:THREAD "clack-handler-woo" {1005306473}>)
Conclusion
To recap, after working your way though this tutorial you should be able to:
- Explain what an environmental variable is
- Explain why environmental variables are important to application security
- Use
cl-dotenv
to load a file containing data to be loaded into the process - Use Lisp code to display the new loaded environmental variable
Github
The link for this tutorial code is available here.
Resources
Joe Marshall — Avoiding Stringly Typed Code
@2025-03-31 07:00 · 34 days agoIt can be tempting to implement certain objects by their printed representation. This is especially true when you call out to other programs and pass the parameters in command line arguments and get a result back through the stdout stream. If an object is implemented by its printed representation, then serialization and deserialization of the object across program boundaries is trivial.
Objects implemented by their printed representation are jokingly referred to as “stringly typed”. The type information is lost so it is possible to pass strings representing objects of the wrong type and get nonsense answers. There are no useful predicates on arbitrary strings, so you cannot do type checking or type dispatch. This becomes a big problem for objects created from other utilities. When you call out to a bash script, you usually get the response as stream or string.
The solution? Slap a type on it right away. For any kind of string
we get back from another program, we at least define a CLOS class
with a single slot that holds a string. I define two Lisp bindings
for any program implemented by a shell script. The one with
a %
prefix is the program that takes and returns
strings. Without the %
it takes and returns Lisp
objects that are marshaled to and from strings before
the %
version is called. The %
version
obviously cannot do type checking, but the non-%
entry
point can and does enforce the runtime type.
For older items, see the Planet Lisp Archives.
Last updated: 2025-05-02 00:00