01 Jan 2026
Planet Lisp
Quicklisp news: January 2026 Quicklisp dist update now available
New projects:
- asdf-dependency-traverser - Easily traverse and collect ASDF dependencies recursively. - zlib
- calendar-times - A calendar times library on top of local-time - MIT
- champ-lite - A lightweight implementation of persistent functional maps and iteration-safe mutable tables using Michael Steindorfer's CHAMP data structure. - Unlicense
- cl-avro - Implementation of the Apache Avro data serialization system. - GPLv3
- cl-chise - CHISE implementation based on Common Lisp - LGPL
- cl-double-metaphone - Common Lisp implementation of the Double Metaphone phonetic algorithm. - Apache 2.0
- cl-freelock - lock-free concurrency primitives, written in pure Common Lisp. - MIT
- cl-inix - cl-inix is a flexible library for .INI/.conf file parsing - BSD-2 Clause
- cl-jsonpath - JSONPath implementation for Common Lisp with 99% test coverage and complete RFC 9535 compliance. Supports cl-json, jonathan, and jzon backends with advanced features including arithmetic expressions, recursive descent, and bracket notation in filters. - MIT
- cl-ktx2 - An implementation of the Khronos KTX Version 2 image file format - zlib
- cl-match-patterns - Describe cl-match-patterns here - BSD-2 Clause
- cl-minifloats - Minifloats (minifloat < single-float) support for Common Lisp - BSD-2 Clause
- cl-sanitize-html - OWASP-style HTML sanitization library for Common Lisp - MIT
- cl-tbnl-gserver-tmgr - Hunchentoot pooled multi-threaded taskmanager based on cl-gserver. - MIT
- cl-tuition - A Common Lisp library for building TUIs - MIT
- cl-turbojpeg - An up-to-date bindings library for the JPEG Turbo C library - zlib
- cl-version-string - Generate version strings. - MIT
- cl-win32-errors - A library for translating Windows API error codes. - MIT
- cleopter - Minimalist command-line parser - MIT
- clq - clq is a package that allows the definition and development of quantum circuits in Common Lisp and to export them to OpenQASM v2.0. - MIT
- collidxr - A collection of syntax sugar and conveniences extending cl-collider, a Common Lisp interface to the SuperCollider sound synthesis server. - MIT
- copimap - IMAP client/sync library - MIT
- dual-numbers - A library for dual numbers in Common Lisp - MIT
- fold - FOLD-LEFT and FOLD-RIGHT - MIT
- function - Higher order functions. - MIT
- generic-arithmetic - A library for generic arithmetic operations - MIT
- hunchentoot-recycling-taskmaster - An experiment to improve multithreading performance of Hunchentoot without any additional dependencies. - BSD 2-Clause
- imagine - A general image decoding and manipulation library - zlib
- json-to-data-frame - This repository provides a Common Lisp library to convert JSON data into a data frame using the `json-to-df` package. The package leverages the `yason` library for JSON parsing and `dfio` for data frame operations. - MIT
- live-cells-cl - A reactive programming library for Lisp - BSD 3-Clause
- named-let - Named LET special form. - MIT
- netaddr - A library for manipulating IP addresses, subnets, ranges, and sets. - MIT
- pantry - Common Lisp client for Pantry JSON storage service: https://getpantry.cloud - BSD
- pira - Unofficial AWS SDK for Common Lisp - MIT
- smithy-lisp - Smithy code generator for Common Lisp - MIT
- star - Štar: an iteration construct - MIT
- trinsic - Common Lisp utility system to aid in extrinsic and intrinsic system construction. - MIT
- trivial-inspect - Portable toolkit for interactive inspectors. - BSD-2 Clause
- trivial-time - trivial-time allows timing a benchmarking a piece of code portably - BSD-2 Clause
Updated projects: 3d-math, 3d-matrices, 3d-quaternions, 3d-spaces, 3d-transforms, 3d-vectors, action-list, adhoc, anypool, array-utils, async-process, atomics, babel, binary-structures, bp, cambl, cari3s, cephes.cl, cffi, cffi-object, chain, chipi, chirp, chunga, cl+ssl, cl-algebraic-data-type, cl-all, cl-batis, cl-bmp, cl-charms, cl-collider, cl-concord, cl-cxx, cl-data-structures, cl-dbi, cl-dbi-connection-pool, cl-decimals, cl-def-properties, cl-duckdb, cl-enchant, cl-enumeration, cl-fast-ecs, cl-fbx, cl-flac, cl-flx, cl-fond, cl-gamepad, cl-general-accumulator, cl-gltf, cl-gobject-introspection-wrapper, cl-gog-galaxy, cl-gpio, cl-html-readme, cl-i18n, cl-jingle, cl-just-getopt-parser, cl-k8055, cl-ktx, cl-las, cl-lc, cl-ledger, cl-lex, cl-liballegro, cl-liballegro-nuklear, cl-libre-translate, cl-markless, cl-migratum, cl-mixed, cl-modio, cl-monitors, cl-mpg123, cl-naive-tests, cl-oju, cl-opengl, cl-opus, cl-out123, cl-protobufs, cl-pslib, cl-qoa, cl-rcfiles, cl-resvg, cl-sf3, cl-soloud, cl-spidev, cl-steamworks, cl-str, cl-svg, cl-transducers, cl-transmission, cl-unification, cl-utils, cl-vorbis, cl-wavefront, cl-wavelets, cl-who, cl-xkb, cl-yacc, cl-yahoo-finance, clad, classimp, classowary, clast, clath, clazy, clingon, clip, clith, clog, clohost, closer-mop, clss, clunit2, clustered-intset, clws, clx, cmd, coalton, cocoas, colored, com-on, com.danielkeogh.graph, concrete-syntax-tree, conduit-packages, consfigurator, crypto-shortcuts, damn-fast-priority-queue, data-frame, data-lens, datafly, datamuse, declt, deeds, defenum, deferred, definer, definitions, deploy, depot, dexador, dfio, dissect, djula, dns-client, doc, documentation-utils, dsm, easy-audio, easy-routes, eclector, esrap, expanders, f2cl, feeder, file-attributes, file-finder, file-lock, file-notify, file-select, filesystem-utils, flare, float-features, flow, font-discovery, for, form-fiddle, format-seconds, fset, functional-trees, fuzzy-dates, fuzzy-match, fxml, gendl, genhash, glfw, glsl-toolkit, graph, harmony, helambdap, hsx, http2, hu.dwim.asdf, hu.dwim.util, hu.dwim.walker, humbler, iclendar, imago, in-nomine, incless, inkwell, inravina, invistra, iterate, journal, jpeg-turbo, jsonrpc, khazern, knx-conn, lack, lambda-fiddle, language-codes, lass, legit, lemmy-api, letv, lichat-ldap, lichat-protocol, lichat-serverlib, lichat-tcp-client, lichat-tcp-server, lichat-ws-server, linear-programming-glpk, lisa, lisp-chat, lisp-interface-library, lisp-stat, lla, local-time, log4cl-extras, logging, lquery, lru-cache, luckless, lunamech-matrix-api, machine-measurements, machine-state, maiden, manifolds, math, mcclim, memory-regions, messagebox, mgl-pax, misc-extensions, mito, mito-auth, mk-defsystem, mmap, mnas-path, modularize, modularize-hooks, modularize-interfaces, multilang-documentation, multiposter, mutility, mutils, named-readtables, neural-classifier, new-op, nodgui, nontrivial-gray-streams, north, numerical-utilities, oclcl, omglib, one-more-re-nightmare, ook, open-location-code, open-with, osicat, overlord, oxenfurt, pango-markup, parachute, parse-float, pathname-utils, peltadot, perceptual-hashes, periods, petalisp, phos, physical-quantities, piping, plot, plump, plump-sexp, plump-tex, postmodern, precise-time, promise, punycode, py4cl2-cffi, qlot, qoi, quaviver, queen.lisp, quickhull, quilc, quri, qvm, random-sampling, random-state, ratify, reblocks, reblocks-websocket, redirect-stream, rove, sc-extensions, scriptl, sel, serapeum, shasht, shop3, si-kanren, simple-inferiors, simple-tasks, slime, sly, softdrink, south, speechless, spinneret, staple, statistics, studio-client, sxql, sycamore, system-locale, terrable, testiere, text-draw, tfeb-lisp-hax, timer-wheel, tooter, trivial-arguments, trivial-benchmark, trivial-download, trivial-extensible-sequences, trivial-indent, trivial-main-thread, trivial-mimes, trivial-open-browser, trivial-package-locks, trivial-thumbnail, trivial-toplevel-prompt, trivial-with-current-source-form, type-templates, uax-14, uax-9, ubiquitous, uncursed, usocket, vellum, verbose, vp-trees, wayflan, websocket-driver, with-contexts, wouldwork, xhtmlambda, yah, zippy.
Removed projects: cl-vhdl, crane, dataloader, diff-match-patch, dso-lex, dso-util, eazy-project, hu.dwim.presentation, hu.dwim.web-server, numcl, orizuru-orm, tfeb-lisp-tools, uuidv7.lisp.
To get this update, use (ql:update-dist "quicklisp")
Enjoy!
01 Jan 2026 5:27pm GMT
31 Dec 2025
Planet Lisp
Joe Marshall: Code mini-golf
Here are some simple puzzles to exercise your brain.
1. Write partial-apply-left, a function that takes a binary function and the left input of the binary function and returns the unary function that takes the right input and then applies the binary function to both inputs.
For example:
;; Define *foo* as a procedure that conses 'a onto its argument. > (defvar *foo* (partial-apply-left #'cons 'a)) > (funcall *foo* 'b) (A . B) > (funcall *foo* 42) (A . 42)
2. Write distribute, a function that takes a binary function, a left input, and a list of right inputs, and returns a list of the results of applying the binary function to the left input and each of the right inputs. (Hint: Use partial-apply-left)
For example:
> (distribute #'cons 'a '( (b c d) e 42)) ((A B C D) (A . E) (A . 42))
3. Write removals, a function that takes a list and returns a list of lists, where each sublist is the original list with exactly one element removed.
For example:
> (removals '(a b c)) ((B C) (A C) (A B))
Hint:
- One removal is the CDR of the list.
- Other removals can be constructed by (distributed) consing the CAR onto the removals of the CDR.
4. Write power-set, a function that takes a list and returns the power set of that list (the set of all subsets of the original list).
For example:
> (power-set '(a b c)) (() (C) (B) (B C) (A) (A C) (A B) (A B C))
Hint:
Note how the power set of a list can be constructed from the power set of its CDR by adding the CAR to each subset in the power set of the CDR.
5. Write power-set-gray that returns the subsets sorted so each subset differs from the previous subset by a change of one element (i.e., each subset is equal to the next subset with either one element added or one element removed). This is called a Gray code ordering of the subsets.
For example:
> (power-set-gray '(a b c)) (() (C) (B C) (B) (A B) (A B C) (A C) (A))
Hint:
When appending the two halves of the power set, reverse the order of the second half.
31 Dec 2025 7:31pm GMT
26 Dec 2025
Planet Lisp
Marco Antoniotti
Retro (?) Computing in Common Lisp: the CL3270 Library
Come the Winter Holidays and, between too much and a lot of food, I do some hacking and maintainance of my libraries.
Some time ago, I wrote a CL library to set up a server accepting and managing "applications" written for a IBM 3270 terminal.
Why did I do this? Because I like to waste time hacking, and because I got a (insane) fascination with mainframe computing. On top of that, on the Mainframe Enthusiasts Discord channel, Matthew R. Wilson posted a recently updated version of my inspiration, the go3270 GO library.
Of course, I had to fall in the rabbit..., ahem, raise to the occasion, and updated the CL3270 library. This required learing a lot about several things, but rendering the GO code in CL is not difficult, once you undestrand how the GO creators applied Greenspun's Tenth Rule of Programming.
Of course there were some quirks that had to be addressed, but the result is pretty nice.
Screenshots
Here are a couple of screenshots.
"Example 3": The Time Ticker
Yes, it works as advertised.
This is how the server is started from **CL** (Lispworks in this case).
... and this is how the c3270 connects and interacts with the server.
"Example 4": The Mock Database
This example has many panels which fake a database application. The underlying implementation use "transactions", that is, a form of continuations.
Starting the server...
... and two of the screens.
It has been fun developing the library and keeping up with the talented Matthew R. Wilson.
Download the CL3270 library (the development branch is more up to speed) and give it a spin if you like.
'(cheers)
26 Dec 2025 5:24pm GMT
18 Dec 2025
Planet Lisp
Eugene Zaikonnikov: Lisp job opening in Bergen, Norway
As a heads-up my employer now has an opening for a Lisp programmer in Bergen area. Due to hands-on nature of developing the distributed hardware product the position is 100% on-prem.
18 Dec 2025 12:00am GMT
11 Dec 2025
Planet Lisp
Scott L. Burson: FSet v2.1.0 released: Seq improvements
I have just released FSet v2.1.0 (also on GitHub).
This release is mostly to add some performance and functionality improvements for seqs. Briefly:
- Access to and updating of elements at the beginning or end of a long seq is now faster.
- I have finally gotten around to implementing
searchandmismatchon seqs. NOTE: this may require changes to your package definitions; see below. - Seqs containing only characters are now treated specially, making them a viable replacement for CL strings in many cases.
- In an FSet 2 context, the seq constructor macros now permit specification of a default.
- There are changes to some
convertmethods. - There are a couple more FSet 2 API changes, involving
image.
See the above links for the full release notes.
UPDATE: there's already a v2.1.1; I had forgotten to export the new function char-seq?.
11 Dec 2025 4:01am GMT
04 Dec 2025
Planet Lisp
Tim Bradshaw: Literals and constants in Common Lisp
Or, constantp is not enough.
Because I do a lot of things with Štar, and for other reasons, I spend a fair amount of time writing various compile-time optimizers for things which have the semantics of function calls. You can think of iterator optimizers in Štar as being a bit like compiler macros: the aim is to take a function call form and to turn it, in good cases, into something quicker1. One important way of doing this is to be able to detect things which are known at compile-time: constants and literals, for instance.
One of the things this has made clear to me is that, like John Peel, constantp is not enough. Here's an example.
(in-row-major-array a :simple t :element-type 'fixnum) is a function call whose values Štar can use to tell it how to iterate (via row-major-aref) over an array. When used in a for form, its optimizer would like to be able to expand into something involving (declare (type (simple-array fixnum *) ...), so that the details of the array are known to the compiler, which can then generate fast code for row-major-aref. This makes a great deal of difference to performance: array access to simple arrays of known element types is usually much faster than to general arrays.
In order to do this it needs to know two things:
- that the values of the
simpleandelement-typekeyword arguments are compile-time constants; - what their values are.
You might say, well, that's what constantp is for2. It's not: constantp tells you only the first of these, and you need both.
Consider this code, in a file to be compiled:
(defconstant et 'fixnum)
(defun ... ...
(for ((e (in-array a :element-type et)))
...)
...)
Now, constantpwill tell you that et is indeed a compile-time constant. But it won't tell you its value, and in particular nothing says it needs to be bound at compile-time at all: (symbol-value 'et) may well be an error at compile-time.
constantp is not enough3! instead you need a function that tells you 'yes, this thing is a compile-time constant, and its value is …'. This is what literal does4: it conservatively answers the question, and tells you the value if so. In particular, an expression like (literal '(quote fixnum)) will return fixnum, the value, and t to say yes, it is a compile-time constant. It can't do this for things defined with defconstant, and it may miss other cases, but when it says something is a compile-time constant, it is. In particular it works for actual literals (hence its name), and for forms whose macroexpansion is a literal.
That is enough in practice.
-
Śtar's iterator optimizers are not compiler macros, because the code they write is inserted in various places in the iteration construct, but they're doing a similar job: turning a construct involving many function calls into one requiring fewer or no function calls. ↩
-
And you may ask yourself, "How do I work this?" / And you may ask yourself, "Where is that large automobile?" / And you may tell yourself, "This is not my beautiful house" / And you may tell yourself, "This is not my beautiful wife" ↩
-
Here's something that staryed as a mail message which tries to explain this in some more detail. In the case of variables
defconstantis required to tellconstantpthat a variable is a constant at compile-time but is not required (and should not be required) to evaluate the initform, let alone actually establish a binding at that time. In SBCL it does both (SBCL doesn't really have a compilation environment). In LW, say, it at least does not establish a binding, because LW does have a compilation environment. That means that in LW compiling a file has fewer compile-time side-effects than it does in SBCL. Outside of variables, it's easily possible that a compiler might be smart enough to know that, given(defun c (n) (+ n 15)), then(constantp '(c 1) <compilation environment>)is true. But you can't evaluate(c 1)at compile-time at all.constantptells you that you don't need to bind variables to prevent multiple evaluation, it doesn't, and can't, tell you what their values will be. ↩ -
Part of the
org.tfeb.star/utilitiespackage. ↩
04 Dec 2025 4:23pm GMT
01 Dec 2025
Planet Lisp
Joe Marshall: Advent of Code 2025
The Advent of Code will begin in a couple of hours. I've prepared a Common Lisp project to hold the code. You can clone it from https://github.com/jrm-code-project/Advent2025.git It contains an .asd file for the system, a package.lisp file to define the package structure, 12 subdirectories for each day's challenge (only 12 problems in this year's calendar), and a file each for common macros and common functions.
As per the Advent of Code rules, I won't use AI tools to solve the puzzles or write the code. However, since AI is now part of my normal workflow these days, I may use it for enhanced web search or for autocompletion.
As per the Advent of Code rules, I won't include the puzzle text or the puzzle input data. You will need to get those from the Advent of Code website (https://adventofcode.com/2025).
01 Dec 2025 12:42am GMT
30 Nov 2025
Planet Lisp
vindarel: Practice for Advent Of Code in Common Lisp
Advent Of Code 2025 starts in a few hours. Time to practice your Lisp-fu to solve it with the greatest language of all times this year!
Most of the times, puzzles start with a string input we have to parse to a meaningful data structure, after which we can start working on the algorithm. For example, parse this:
(defparameter *input* "3 4
4 3
2 5
1 3
3 9
3 3")
into a list of list of integers, or this:
(defparameter *input* "....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...")
into a grid, a map. But how do you represent it, how to do it efficiently, what are the traps to avoid, are there some nice tricks to know? We'll try together.
You'll find those 3 exercises of increasing order also in the GitHub repository of my course (see my previous post on the new data structures chapter).
I give you fully-annotated puzzles and code layout. You'll have to carefully read the instructions, think about how you would solve it yourself, read my proposals, and fill-in the blanks -or do it all by yourself. Then, you'll have to check your solution with your own puzzle input you have to grab from AOC's website!
Table of Contents
- Prerequisites
- Exercise 1 - lists of lists
- Exercise 2 - prepare to parse a grid as a hash-table
- Harder puzzle - hash-tables, grid, coordinates
- Closing words
Prerequisites
You must know the basics, but not so much. And if you are an experienced Lisp developer, you can still find new constraints for this year: solve it with loop, without loop, with a purely-functional data structure library such as FSet, use Coalton, create animations, use the object system, etc.
If you are starting out, you must know at least:
- the basic data structures (lists and their limitations, arrays and vectors, hash-tables, sets...)
- iteration (iterating over a list, arrays and hash-table keys)
- functions
no need of macros, CLOS or thorough error handling (it's not about production-grade puzzles :p ).
Exercise 1 - lists of lists
This exercise comes from Advent Of Code 2024, day 01: https://adventofcode.com/2024/day/1
Read the puzzle there! Try with your own input data!
Here are the shortened instructions.
;;;
;;; ********************************************************************
;;; WARN: this exercise migth be hard if you don't know about functions.
;;; ********************************************************************
;;;
;;; you can come back to it later.
;;; But, you can have a look, explore and get something out of it.
In this exercise, we use:
;;; SORT
;;; ABS
;;; FIRST, SECOND
;;; EQUAL
;;; LOOP, MAPCAR, REDUCE to iterate and act on lists.
;;; REMOVE-IF
;;; PARSE-INTEGER
;;; UIOP (built-in) and a couple string-related functions
;;;
;;; and also:
;;; feature flags
;;; ERROR
;;;
;;; we don't rely on https://github.com/vindarel/cl-str/
;;; (nor on cl-ppcre https://common-lisp-libraries.readthedocs.io/cl-ppcre/)
;;; but it would make our life easier.
;;;
OK, so this is your puzzle input, a string representing two colums of integers.
(defparameter *input* "3 4
4 3
2 5
1 3
3 9
3 3")
We'll need to parse this string into two lists of integers.
If you want to do it yourself, take the time you need! If you're new to Lisp iteration and data structures, I give you a possible solution.
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;; [hiding in case you want to do it...]
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
;;;
(defun split-lines (s)
"Split the string S by newlines.
Return: a list of strings."
;; If you already quickloaded the STR library, see:
;; (str:lines s)
;;
;; UIOP comes with ASDF which comes with your implementation.
;; https://asdf.common-lisp.dev/uiop.html
;;
;; #\ is a built-in reader-macro to write a character by name.
(uiop:split-string s :separator '(#\Newline)))
Compile the function and try it on the REPL, or with a quick test expression below a "feature flag".
We get a result like '("3 4" "4 3" "2 5" "1 3" "3 9" "3 3"), that is a list of strings with numbers inside.
#+lets-try-it-out
;; This is a feature-flag that looks into this keyword in the top-level *features* list.
;; The expression below should be highlihgted in grey
;; because :lets-try-it-out doesn't exist in your *features* list.
;;
;; You can compile this with C-c C-c
;; Nothing should happen.
(assert (equal '("3 4" "4 3" "2 5" "1 3" "3 9" "3 3")
(split-lines *input*)))
;; ^^ you can put the cursor here and eval the expression with C-x C-e, or send it to the REPL with C-c C-j.
We now have to extract the integers inside each string.
To do this I'll use a utility function.
;; We could inline it.
;; But, measure before trying any speed improvement.
(defun blank-string-p (s)
"S is a blank string (no content)."
;; the -p is for "predicate" (returns nil or t (or a truthy value)), it's a convention.
;;
;; We already have str:blankp in STR,
;; and we wouldn't need this function if we used str:words.
(equal "" s)) ;; better: pair with string-trim.
#+(or)
(blank-string-p nil)
#++
(blank-string-p 42)
#+(or)
(blank-string-p "")
And another one, to split by spaces:
(defun split-words (s)
"Split the string S by spaces and only return non-blank results.
Example:
(split-words \"3 4\")
=> (\"3\" \"4\")
"
;; If you quickloaded the STR library, see:
;; (str:words s)
;; which actually uses cl-ppcre under the hood to split by the \\s+ regexp,
;; and ignore consecutive whitespaces like this.
;;
(let ((strings (uiop:split-string s :separator '(#\Space))))
(remove-if #'blank-string-p strings)))
#+lets-try-it-out
;; test this however you like.
(split-words "3 4")
I said we wouldn't use a third-party library for this first puzzle. But using cl-ppcre would be so much easier:
(ppcre:all-matches-as-strings "\\d+" "3 6")
;; => ("3" "6")
With our building blocks, this is how I would parse our input string into a list of list of integers.
We loop on input lines and use the built-in function parse-integer.
(defun parse-input (input)
"Parse the multi-line INPUT into a list of two lists of integers."
;; loop! I like loop.
;; We see everything about loop in the iteration chapter.
;;
;; Here, we see one way to iterate over lists:
;; loop for ... in ...
;;
;; Oh, you can rewrite it in a more functional style if you want.
(loop :for line :in (split-lines input)
:for words := (split-words line)
:collect (parse-integer (first words)) :into col1
:collect (parse-integer (second words)) :into col2
:finally (return (list col1 col2))))
#+lets-try-it-out
(parse-input *input*)
;; ((3 4 2 1 3 3) (4 3 5 3 9 3))
The puzzle continues.
"Maybe the lists are only off by a small amount! To find out, pair up the numbers and measure how far apart they are. Pair up the smallest number in the left list with the smallest number in the right list, then the second-smallest left number with the second-smallest right number, and so on."
=> we need to SORT the columns by ascending order.;;;
"Within each pair, figure out how far apart the two numbers are;"
=> we need to compute their relative, absolute distance.
"you'll need to add up all of those distances."
=> we need to sum each relative distance.
"For example, if you pair up a 3 from the left list with a 7 from the right list, the distance apart is 4; if you pair up a 9 with a 3, the distance apart is 6."
Our input data's sum of the distances is 11.
We must sort our lists of numbers. Here's a placeholder function:
(defun sort-columns (list-of-lists)
"Accept a list of two lists.
Sort each list in ascending order.
Return a list of two lists, each sorted."
;; no mystery, use the SORT function.
(error "not implemented"))
;; Use this to check your SORT-COLUMNS function.
;; You can write this in a proper test function if you want.
#+lets-try-it-out
(assert (equal (sort-columns (parse-input *input*))
'((1 2 3 3 3 4) (3 3 3 4 5 9))))
Compute the absolute distance.
;; utility function.
(defun distance (a b)
"The distance between a and b.
Doesn't matter if a < b or b < a."
;;
;; hint: (abs -1) is 1
;;
(error "not implemented")
)
(defun distances (list-of-lists)
"From a list of two lists, compute the absolute distance between each point.
Return a list of integers."
(error "not implemented")
;; hint:
;; (mapcar #'TODO (first list-of-lists) (second list-of-lists))
;;
;; mapcar is a functional-y way to iterate over lists.
)
(defun sum-distances (list-of-integers)
"Add the numbers in this list together."
(error "not implemented")
;; Hint:
;; try apply, funcall, mapcar, reduce.
;; (TODO #'+ list-of-integers)
;; or loop ... sum !
)
Verify.
(defun solve (&optional (input *input*))
;; let it flow:
(sum-distances (distances (sort-columns (parse-input input)))))
#+lets-try-it-out
(assert (equal 11 (solve)))
All good? There's more if you want.
;;;
;;; Next:
;;; - do it with your own input data!
;;; - do the same with the STR library and/or CL-PPCRE.
;;; - write a top-level instructions that calls our "main" function so that you can call this file as a script from the command line, with sbcl --load AOC-2024-day01.lisp
;;;
Exercise 2 - prepare to parse a grid as a hash-table
This exercise is a short and easy, to prepare you for a harder puzzle. This is not an AOC puzzle itself.
Follow the instructions. We are only warming up.
;; Do this with only CL built-ins,
;; or with the dict notation from Serapeum,
;; or with something else,
;; or all three one after the other.
We will build up a grid stored in a hash-table to represent a map like this:
"....#...##....#"
where the # character represents an obstacle.
In our case the grid is in 1D, it is often rather 2D.
This grid/map is the base of many AOC puzzles.
Take a second: shall we represent a 2D grid as a list of lists, or something else, (it depends on the input size) and how would you do in both cases?
Your turn:
;;
;; 1. Define a function MAKE-GRID that returns an empty grid (hash-table).
;;
(defun make-grid ()
;; todo
)
;;
;; Define a top-level parameter to represent a grid that defaults to an empty grid.
;;
;; def... *grid* ...
;;
;; 2. Create a function named CELL that returns a hash-table with those keys:
;; :char -> holds the character of the grid at this coordinate.
;; :visited or :visited-p or even :visited? -> stores a boolean,
;; to tell us if this cell was already visited (by a person walking in the map). It defaults
;; to NIL, we don't use this yet.
;;
(defun cell (char &key visited)
;; todo
)
;;
;; 3. Write a function to tell us if a cell is an obstacle,
;; denoted by the #\# character
;;
(defun is-block (cell)
"This cell is a block, an obstacle. Return: boolean."
;; todo
;; get the :char key,
;; check it equals the #\# char.
;; Accept a cell as NIL.
)
We built utility functions we'll likely re-use on a more complex puzzle.
Let's continue with parsing the input to represent a grid.
If you are a Lisp beginner or only saw the data structures chapter in my course, I give you the layout of the parse-input function with a loop and you only have to fill-in one blank.
In any case, try yourself. Refer to the Cookbook for loop examples.
;;
;; 4. Fill the grid (with devel data).
;;
;; Iterate on a given string (the puzzle input),
;; create the grid,
;; keep track of the X coordinate,
;; for each character in the input create a cell,
;; associate the coordinate to this cell in the grid.
;;
(defparameter *input* ".....#..#.##...#........##...")
(defun parse-grid (input)
"Parse a string of input, fill a new grid with a coordinate number -> a cell (hash-table).
Return: our new grid."
(loop :for char :across input
:with grid := (make-grid)
:for x :from 0
:for cell := (cell char)
:do
;; associate our grid at the X coordinate
;; with our new cell.
;; (setf ... )
:finally (return grid)))
;; try it:
#++
(parse-grid *input*)
That's only a simple example of the map mechanism that comes regurlarly in AOC.
Here's the 3rd exercise that uses all of this.
Harder puzzle - hash-tables, grid, coordinates
This exercise comes from Advent Of Code 2024, day 06. https://adventofcode.com/2024/day/6 It's an opportunity to use hash-tables.
Read the puzzle there! Try with your own input data!
Here are the shortened instructions.
The solutions are in another file, on my GitHub repository.
;;;
;;; ********************************************************************
;;; WARN: this exercise migth be hard if you don't know about functions.
;;; ********************************************************************
;;;
;;; you can come back to it later.
;;; But, you can have a look, explore and get something out of it.
In this exercise, we use:
;;;
;;; parameters
;;; functions
;;; recursivity
;;; &aux in a lambda list
;;; CASE
;;; return-from
;;; &key arguments
;;; complex numbers
;;; hash-tables
;;; the DICT notation (though optional)
;;; LOOPing on a list and on strings
;;; equality for characters
For this puzzle, we make our life easier and we' use the DICT notation.
(import 'serapeum:dict)
If you know how to create a package, go for it.
Please, quickload the STR library for this puzzle.
#++
(ql:quickload "str")
;; Otherwise, see this as another exercise to rewrite the functions we use.
This is your puzzle input:
;;; a string representing a grid, a map.
(defparameter *input* "....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...")
;; the # represents an obstacle,
;; the ^ represents a guard that walks to the top of the grid.
When the guard encounters an obstacle, it turns 90 degrees right, and keeps walking.
Our task is to count the number of distinct positions the guard will visit on the grid before eventually leaving the area.
We will have to: - parse the grid into a data structure - preferably, an efficient data structures to hold coordinates. Indeed, AOC real inputs are large. - for each cell, note if it's an obstacle, if that's where the guard is, if the cell was already visited, - count the number of visited cells.
;; We'll represent a cell "object" by a hash-table.
;; With Serapeum's dict:
(defun cell (char &key guard visited)
(dict :char char
:guard guard
:visited visited))
;; Our grid is a dict too.
;; We create a top-level variable, mainly for devel purposes.
(defvar *grid* (dict)
"A hash-table to represent our grid. Associates a coordinate (complex number which represents the X and Y axis in the same number) to a cell (another hash-table).")
;; You could use a DEFPARAMETER, like I did initially. But then, a C-c C-k (recompile current file) will erase its current value, and you might want or not want this.
For each coordinate, we associate a cell.
What is a coordinate? We use a trick we saw in other people's AOC solution, to use a complex number. Indeed, with its real and imaginary parts, it can represent both the X axis and the Y axis at the same time in the same number.
#|
;; Practice complex numbers:
(complex 1)
;; => 1
(complex 1 1)
;; => represented #C(1 1)
;; Get the imaginary part (let's say, the Y axis):
(imagpart #C(1 1))
;; the real part (X axis):
(realpart #C(1 1))
|#
Look, we are tempted to go full object-oriented and represent a "coordinate" object, a "cell" object and whatnot, but it's OK we can solve the puzzle with usual data structures.
;; Let's remember where our guard is.
(defvar *guard* nil
"The guard coordinate. Mainly for devel purposes (IIRC).")
Task 1: parse the grid string.
We must parse the string to a hash-table of coordinates -> cells.
I'll write the main loop for you. If you feel ready, take a go at it.
(defun parse-grid (input)
"Parse INPUT (string) to a hash-table of coordinates -> cells."
;; We start by iterating on each line.
(loop :for line :in (str:lines input)
;; start another variable that tracks our loop iteration.
;; It it incremented by 1 at each loop by default.
:for y :from 0 ;; up and down on the map, imagpart of our coordinate number.
;; The loop syntax with ... = ... creates a variable at the first iteration,
;; not at every iteration.
:with grid = (dict)
;; Now iterate on each line's character.
;; A string is an array of characters,
;; so we use ACROSS to iterate on it. We use IN to iterate on lists.
;;
;; The Iterate library has the generic :in-sequence clause if that's your thing (with a speed penalty).
:do (loop :for char :across line
:for x :from 0 ;; left to right on the map, realpart of our coordinate.
:for key := (complex x y)
;; Create a new cell at each character.
:for cell := (cell char)
;; Is this cell the guard at the start position?
:when (equal char #\^)
:do (progn
;; Here, use SETF on GETHASH
;; to set the :guard keyword of the cell to True.
(print "we saw the guard")
;; (setf (gethash ... ...) ...)
;; For devel purposes, we will also keep track of
;; where our guard is with a top-level parameter.
(setf *guard* key)
)
:do
;; Normal case:
;; use SETF on GETHAH
;; to associate this KEY to this CELL in our GRID.
(format t "todo: save the cell ~S in the grid" cell)
)
:finally (return grid))
)
;; devel: test and bind a top-level param for ease of debugging/instropection/poking around.
#++
(setf *grid* (parse-grid *input*))
Task 2: walk our guard, record visited cells.
We have to move our guard on the grid, until it exits it.
I'll give you a couple utility functions.
(defun is-block (cell)
"Is this cell an obstacle?"
;; accept a NIL, we'll stop the walk in the next iteration.
(when cell
(equal TODO #\#)))
;; We choose the write the 4 possible directions as :up :down :right :left.
;; See also:
;; exhaustiveness checking at compile-time:
;; https://dev.to/vindarel/compile-time-exhaustiveness-checking-in-common-lisp-with-serapeum-5c5i
(defun next-x (position direction)
"From a position (complex number) and a direction, compute the next X."
(case direction
(:up (realpart position))
(:down (realpart position))
(:right (1+ (realpart position)))
(:left (1- (realpart position)))))
(defun next-y (position direction)
"From a position (complex number) and a direction, compute the next Y."
(case direction
(:up (1- (imagpart position)))
(:down (1+ (imagpart position)))
(:right (imagpart position))
(:left (imagpart position))))
This is the "big" function that moves the guard, records were it went, makes it rotate if it is against a block, and iterates, until the guard goes out of the map.
Read the puzzle instructions carefuly and write the "TODO" placeholders.
(defun walk (&key (grid *grid*) (input *input*)
(position *guard*)
(cell (gethash *guard* *grid*)) ;; todo: *grid* is used here. Fix it so we don't use a top-level variable, but only the grid given as a key argument.
(direction :up)
(count 0)
;; &aux notation: it saves a nested of LET bindings.
;; It's old style.
;; Those are not arguments to the function we pass around,
;; they are bindings inside the function body.
&aux next-cell
next-position
obstacle-coming)
"Recursively move the guard and annotate cells of our grid,
count the number of visited cells."
;; At each iteration, we study a new cell we take on our grid.
;; If we move the guard to a coordinate that doesn't exist in our grid,
;; we stop here.
(unless cell
(return-from walk count))
;; Look in the same direction first and see what we have.
(setf next-position
(complex (next-x position direction) (next-y position direction)))
(setf next-cell (gethash next-position grid))
;; obstacle?
(setf obstacle-coming (is-block next-cell))
;; then change direction.
(when obstacle-coming
(setf direction
(case direction
(:up :right)
(:down :left)
(:right :down)
(:left :up))))
;; Count unique visited cells.
;; TODO
(unless (print "if this CELL is visited...")
(incf count)
;; TODO set this cell as visited.
(print "set this CELL to visited")
)
;; get our next position now.
(setf next-position
(complex (next-x position direction) (next-y position direction)))
;; This next cell may or may not be in our grid (NIL).
(setf next-cell (gethash next-position grid))
(walk :grid grid :input input
:cell next-cell
:position next-position
:direction direction
:count count))
and that's how we solve the puzzle:
(defun part-1 (input)
(walk :grid (parse-grid input)))
#++
(part-1 *input*)
;; 41
;; The right answer for this input.
;; In AOC, you have a bigger, custom puzzle input. This can lead to surprises.
Closing words
Look at other people's solutions too. For example, ak-coram's for our last exercise (using FSet). See how Screamer is used for day 06 by bo-tato (reddit). atgreen (ocicl, cl-tuition, cffi...) solution with a grid as a hash-table with complex numbers. lispm's day 04 solution. Can you read all solutions?
On other days, I used:
- alexandria's
map-permutationsfor day 08 when you want... permutations. It doesn't "cons" (what does that mean you ask? You didn't follow my course ;) ). Read here: https://dev.to/vindarel/advent-of-code-alexandrias-map-permutations-was-perfect-for-day-08-common-lisp-tip-16il. - the library fare-memoization, to help in a recursive solution.
- to write math, use cmu-infix. When you spot 2 equations with 2 unknows, think "Cramer system". This came up last year, so maybe not this year.
- with very large numbers: use double floats, as in
1.24d0 - least common multiple?
lcmis a built-in. - str:match can be a thing to parse strings.
- if you got CIEL (CIEL Is an Extended Lisp), you have Alexandria, cl-str, Serapeum:dict and more libraries baked-in. It's also an easy way to run Lisp scripts (with these dependencies) from the shell.
See you and happy lisping!
Your best resources:
30 Nov 2025 6:12pm GMT
28 Nov 2025
Planet Lisp
TurtleWare: Common Lisp and WebAssembly
Table of Contents
Using Common Lisp in WASM enabled runtimes is a new frontier for the Common Lisp ecosystem. In the previous post Using Common Lisp from inside the Browser I've discussed how to embed Common Lisp scripts directly on the website, discussed the foreign function interface to JavaScript and SLIME port called LIME allowing the user to connect with a local Emacs instance.
This post will serve as a tutorial that describes how to build WECL and how to cross-compile programs to WASM runtime. Without further ado, let's dig in.
Building ECL
To compile ECL targeting WASM we first build the host version and then we use it to cross-compile it for the target architecture.
git clone https://gitlab.com/embeddable-common-lisp/ecl.git
cd ecl
export ECL_SRC=`pwd`
export ECL_HOST=${ECL_SRC}/ecl-host
./configure --prefix=${ECL_HOST} && make -j32 && make install
Currently ECL uses Emscripten SDK that implements required target primitives like libc. In the meantime, I'm also porting ECL to WASI, but it is not ready yet. In any case we need to install and activate emsdk:
git clone https://github.com/emscripten-core/emsdk.git
pushd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
popd
Finally it is time to build the target version of ECL. A flag --disable-shared is optional, but keep in mind that cross-compilation of user programs is a new feature and it is still taking shape. Most notably some nuances with compiling systems from .asd files may differ depending on the flag used here.
make distclean # removes build/ directory
export ECL_WASM=${ECL_SRC}/ecl-wasm
export ECL_TO_RUN=${ECL_HOST}/bin/ecl
emconfigure ./configure --host=wasm32-unknown-emscripten --build=x86_64-pc-linux-gnu \
--with-cross-config=${ECL_SRC}/src/util/wasm32-unknown-emscripten.cross_config \
--prefix=${ECL_WASM} --disable-shared --with-tcp=no --with-cmp=no
emmake make -j32 && emmake make install
# some files need to be copied manually
cp build/bin/ecl.js build/bin/ecl.wasm ${ECL_WASM}
Running from a browser requires us to host the file. To spin Common Lisp web server on the spot, we can use one of our scripts (that assume that quicklisp is installed to download hunchentoot).
export WEBSERVER=${ECL_SRC}/src/util/webserver.lisp
${ECL_TO_RUN} --load $WEBSERVER
# After the server is loaded run:
# firefox localhost:8888/ecl-wasm/ecl.html
Running from node is more straightforward from the console perspective, but there is one caveat: read operations are not blocking, so if we try to run a default REPL we'll have many nested I/O errors because stdin returns EOF. Running in batch mode works fine though:
node ecl-wasm/ecl.js --eval '(format t "Hello world!~%")' --eval '(quit)'
warning: unsupported syscall: __syscall_prlimit64
Hello world!
program exited (with status: 0), but keepRuntimeAlive() is set (counter=0) due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)
The produced wasm is not suitable for running in other runtimes, because Emscripten requires additional functions to emulate setjmp. For example:
wasmedge ecl-wasm/ecl.wasm
[2025-11-21 13:34:54.943] [error] instantiation failed: unknown import, Code: 0x62
[2025-11-21 13:34:54.943] [error] When linking module: "env" , function name: "invoke_iii"
[2025-11-21 13:34:54.943] [error] At AST node: import description
[2025-11-21 13:34:54.943] [error] This may be the import of host environment like JavaScript or Golang. Please check that you've registered the necessary host modules from the host programming language.
[2025-11-21 13:34:54.943] [error] At AST node: import section
[2025-11-21 13:34:54.943] [error] At AST node: module
Building WECL
The previous step allowed us to run vanilla ECL. Now we are going to use artifacts created during the compilation to create an application that skips boilerplate provided by vanilla Emscripten and includes Common Lisp code for easier development - FFI to JavaScript, windowing abstraction, support for <script type='common-lisp'>, Emacs connectivity and in-browser REPL support.
First we need to clone the WECL repository:
fossil clone https://fossil.turtleware.eu/wecl
cd wecl
Then we need to copy over compilation artifacts and my SLIME fork (pull request) to the Code directory:
pushd Code
cp -r ${ECL_WASM} wasm-ecl
git clone git@github.com:dkochmanski/slime.git
popd
Finally we can build and start the application:
./make.sh build
./make.sh serve
If you want to connect to Emacs, then open the file App/lime.el, evaluate the buffer and call the function (lime-net-listen "localhost" 8889). Then open a browser at http://localhost:8888/slug.html and click "Connect". A new REPL should pop up in your Emacs instance.
It is time to talk a bit about contents of the wecl repository and how the instance is bootstrapped. These things are still under development, so details may change in the future.
- Compile
wecl.wasmand its loaderwecl.js
We've already built the biggest part, that is ECL itself. Now we link libecl.a, libeclgc.a and libeclgmp.a with the file Code/wecl.c that calls cl_boot when the program is started. This is no different from the ordinary embedding procedure of ECL.
The file wecl.c defines additionally supporting functions for JavaScript interoperation that allow us to call JavaScript and keeping track of shared objects. These functions are exported so that they are available in CL env. Moreover it loads a few lisp files:
- Code/packages.lisp: package where JS interop functions reside
- Code/utilities.lisp: early utilities used in the codebase (i.e
when-let) - Code/wecl.lisp: JS-FFI, object registry and a stream to wrap
console.log - Code/jsapi/*.lisp: JS bindings (operators, classes, …)
- Code/script-loader.lisp: loading Common Lisp scripts directly in HTML
After that the function returns. It is the user responsibility to start the program logic in one of scripts loaded by the the script loader. There are a few examples of this:
- main.html: loads a repl and another xterm console (external dependencies)
- easy.html: showcase how to interleave JavaScript and Common Lisp in gadgets
- slug.html: push button that connects to the lime.el instance on localhost
The only requirement for the website to use ECL is to include two scripts in its header. boot.js configures the runtime loader and wecl.js loads wasm file:
<!doctype html>
<html>
<head>
<title>Web Embeddable Common Lisp</title>
<script type="text/javascript" src="boot.js"></script>
<script type="text/javascript" src="wecl.js"></script>
</head>
<body>
<script type="text/common-lisp">
(loop for i from 0 below 3
for p = (|createElement| "document" "p")
do (setf (|innerText| p) (format nil "Hello world ~a!" i))
(|appendChild| "document.body" p))
</script>
</body>
</html>
I've chosen to use unmodified names of JS operators in bindings to make looking them up easier. One can use an utility lispify-name to have lispy bindings:
(macrolet ((lispify-operator (name)
`(defalias ,(lispify-name name) ,name))
(lispify-accessor (name)
(let ((lisp-name (lispify-name name)))
`(progn
(defalias ,lisp-name ,name)
(defalias (setf ,lisp-name) (setf ,name))))))
(lispify-operator |createElement|) ;create-element
(lispify-operator |appendChild|) ;append-child
(lispify-operator |removeChild|) ;remove-child
(lispify-operator |replaceChildren|) ;replace-children
(lispify-operator |addEventListener|) ;add-event-listener
(lispify-accessor |innerText|) ;inner-text
(lispify-accessor |textContent|) ;text-content
(lispify-operator |setAttribute|) ;set-attribute
(lispify-operator |getAttribute|)) ;get-attribute
Note that scripts may be modified without recompiling WECL. On the other hand files that are loaded at startup (along with swank source code) are embedded in the wasm file. For now they are loaded at startup, but they may be compiled in the future if there is such need.
When using WECL in the browser, functions like compile-file and compile are available and they defer compilation to the bytecodes compiler. The bytecodes compiler in ECL is very fast, but produces unoptimized bytecode because it is a one-pass compiler. When performance matters, it is necessary to use compile on the host to an object file or to a static library and link it against WECL in file make.sh - recompilation of wecl.wasm is necessary.
Building user programs
Recently Marius Gerbershagen improved cross-compilation support for user programs from the host implementation using the same toolchain that builds ECL. Compiling files simple: use target-info.lisp file installed along with the cross-compiled ECL as an argument to with-compilation-unit:
;;; test-file-1.lisp
(in-package "CL-USER")
(defmacro twice (&body body) `(progn ,@body ,@body))
;;; test-file-1.lisp
(in-package "CL-USER")
(defun bam (x) (twice (format t "Hello world ~a~%" (incf x))))
(defvar *target*
(c:read-target-info "/path/to/ecl-wasm/target-info.lsp"))
(with-compilation-unit (:target *target*)
(compile-file "test-file-1.lisp" :system-p t :load t)
(compile-file "test-file-2.lisp" :system-p t)
(c:build-static-library "test-library"
:lisp-files '("test-file-1.o" "test-file-2.o")
:init-name "init_test"))
This will produce a file libtest-library.a. To use the library in WECL we should include it in the emcc invocation in make.sh and call the function init_test in Code/wecl.c before script-loader.lisp is loaded:
/* Initialize your libraries here, so they can be used in user scripts. */
extern void init_test(cl_object);
ecl_init_module(NULL, init_test);
Note that we've passed the argument :load to compile-file - it ensures that after the file is compiled, we load it (in our case - its source code) using the target runtime *features* value. During cross-compilation ECL includes also a feature :cross. Loading the first file is necessary to define a macro that is used in the second file. Now if we open REPL in the browser:
> #'lispify-name
#<bytecompiled-function LISPIFY-NAME 0x9f7690>
> #'cl-user::bam
#<compiled-function COMMON-LISP-USER::BAM 0x869d20>
> (cl-user::bam 3)
Hello world 4
Hello world 5
Extending ASDF
The approach for cross-compiling in the previous section is the API provided by ECL. It may be a bit crude for everyday work, especially when we work with a complex dependency tree. In this section we'll write an extension to ASDF that allows us to compile entire system with its dependencies into a static library.
First let's define a package and add configure variables:
(defpackage "ASDF-ECL/CC"
(:use "CL" "ASDF")
(:export "CROSS-COMPILE" "CROSS-COMPILE-PLAN" "CLEAR-CC-CACHE"))
(in-package "ASDF-ECL/CC")
(defvar *host-target*
(c::get-target-info))
#+(or)
(defvar *wasm-target*
(c:read-target-info "/path/to/ecl-wasm/target-info.lsp"))
(defparameter *cc-target* *host-target*)
(defparameter *cc-cache-dir* #P"/tmp/ecl-cc-cache/")
ASDF operates in two passes - first it computes the operation plan and then it performs it. To help with specifying dependencies ASDF provides five mixins:
-
DOWNWARD-OPERATION: before operating on the component, perform an operation on children - i.e loading the system requires loading all its components.
-
UPWARD-OPERATION: before operating on the component, perform an operation on parent - i.e invalidating the cache requires invalidating cache of parent.
-
SIDEWAY-OPERATION: before operating on the component, perform the operation on all component dependencies - i.e load components that we depend on
-
SELFWARD-OPERATION: before operating on the component, perform operations on itself - i.e compile the component before loading it
-
NON-PROPAGATING-OPERATION: a standalone operation with no dependencies
Cross-compilation requires us to produce object file from each source file of the target system and its dependencies. We will achieve that by defining two operations: cross-object-op for producing object files from lisp source code and cross-compile-op for producing static libraries from objects:
(defclass cross-object-op (downward-operation) ())
(defmethod downward-operation ((self cross-object-op))
'cross-object-op)
;;; Ignore all files that are not CL-SOURCE-FILE.
(defmethod perform ((o cross-object-op) (c t)))
(defmethod perform ((o cross-object-op) (c cl-source-file))
(let ((input-file (component-pathname c))
(output-file (output-file o c)))
(multiple-value-bind (output warnings-p failure-p)
(compile-file input-file :system-p t :output-file output-file)
(uiop:check-lisp-compile-results output warnings-p failure-p
"~/asdf-action::format-action/"
(list (cons o c))))))
(defclass cross-compile-op (sideway-operation downward-operation)
())
(defmethod perform ((self cross-compile-op) (c system))
(let* ((system-name (primary-system-name c))
(inputs (input-files self c))
(output (output-file self c))
(init-name (format nil "init_lib_~a"
(substitute #\_ nil system-name
:test (lambda (x y)
(declare (ignore x))
(not (alpha-char-p y)))))))
(c:build-static-library output :lisp-files inputs
:init-name init-name)))
(defmethod sideway-operation ((self cross-compile-op))
'cross-compile-op)
(defmethod downward-operation ((self cross-compile-op))
'cross-object-op)
We can confirm that the plan is computed correctly by running it on a system with many transient dependencies:
(defun debug-plan (system)
(format *debug-io* "-- Plan for ~s -----------------~%" system)
(map nil (lambda (a)
(format *debug-io* "~24a: ~a~%" (car a) (cdr a)))
(asdf::plan-actions
(make-plan 'sequential-plan 'cross-compile-op system))))
(debug-plan "mcclim")
In Common Lisp the compilation of subsequent files often depends on previous definitions. That means that we need to load files. Loading files compiled for another architecture is not an option. Moreover:
- some systems will have different dependencies based on features
- code may behave differently depending on the evaluation environment
- compilation may require either host or target semantics for cross-compilation
There is no general solution except from full target emulation or the client code being fully aware that it is being cross compiled. That said, surprisingly many Common Lisp programs can be cross-compiled without many issues.
In any case we need to be able to load source code while it is being compiled. Depending on the actual code we may want to specify the host or the target features, load the source code directly or first compile it, etc. To allow user choosing the load strategy we define an operation cross-load-op:
(defparameter *cc-load-type* :minimal)
(defvar *cc-last-load* :minimal)
(defclass cross-load-op (non-propagating-operation) ())
(defmethod operation-done-p ((o cross-load-op) (c system))
(and (component-loaded-p c)
(eql *cc-last-load* *cc-load-type*)))
;;; :FORCE :ALL is excessive. We should store the compilation strategy flag as a
;;; compilation artifact and compare it with *CC-LOAD-TYPE*.
(defmethod perform ((o cross-load-op) (c system))
(setf *cc-last-load* *cc-load-type*)
(ecase *cc-load-type*
(:emulate
(error "Do you still believe in Santa Claus?"))
(:default
(operate 'load-op c))
(:minimal
(ext:install-bytecodes-compiler)
(operate 'load-op c)
(ext:install-c-compiler))
(:ccmp-host
(with-compilation-unit (:target *host-target*)
(operate 'load-op c :force :all)))
(:bcmp-host
(with-compilation-unit (:target *host-target*)
(ext:install-bytecodes-compiler)
(operate 'load-op c :force :all)
(ext:install-c-compiler)))
(:bcmp-target
(with-compilation-unit (:target *cc-target*)
(ext:install-bytecodes-compiler)
(operate 'load-op c :force :all)
(ext:install-c-compiler)))
(:load-host
(with-compilation-unit (:target *host-target*)
(operate 'load-source-op c :force :all)))
(:load-target
(with-compilation-unit (:target *cc-target*)
(operate 'load-source-op c :force :all)))))
To estabilish a cross-compilation dynamic context suitable for ASDF operations we'll define a new macro WITH-ASDF-COMPILATION-UNIT. It modifies the cache directory, injects features that are commonly expected by various systems, and configures the ECL compiler. That macro is used while the
;;; KLUDGE some system definitions test that *FEATURES* contains this or that
;;; variant of :ASDF* and bark otherwise.
;;;
;;; KLUDGE systems may have DEFSYSTEM-DEPENDS-ON that causes LOAD-ASD to try to
;;; load the system -- we need to modify *LOAD-SYSTEM-OPERATION* for that. Not
;;; to be conflated with CROSS-LOAD-UP.
;;;
;;; KLUDGE We directly bind ASDF::*OUTPUT-TRANSLATIONS* because ASDF advertised
;;; API does not work.
(defmacro with-asdf-compilation-unit (() &body body)
`(with-compilation-unit (:target *cc-target*)
(flet ((cc-path ()
(merge-pathnames "**/*.*"
(uiop:ensure-directory-pathname *cc-cache-dir*))))
(let ((asdf::*output-translations* `(((t ,(cc-path)))))
(*load-system-operation* 'load-source-op)
(*features* (remove-duplicates
(list* :asdf :asdf2 :asdf3 :asdf3.1 *features*))))
,@body))))
Note that loading the system should happen in a different environment than compiling it. Most notably we can't reuse the cache. That's why cross-load-op must not be a dependency of cross-compile-op. Output translations and features affect the planning phase, so we need estabilish the environment over operate and not only perform. We will also define functions for the user to invoke cross-compilation, to show cross-compilation plan and to wipe the cache:
(defun cross-compile (system &rest args
&key cache-dir target load-type &allow-other-keys)
(let ((*cc-cache-dir* (or cache-dir *cc-cache-dir*))
(*cc-target* (or target *cc-target*))
(*cc-load-type* (or load-type *cc-load-type*))
(cc-operation (make-operation 'cross-compile-op)))
(apply 'operate cc-operation system args)
(with-asdf-compilation-unit () ;; ensure cache
(output-file cc-operation system))))
(defun cross-compile-plan (system target)
(format *debug-io* "-- Plan for ~s -----------------~%" system)
(let ((*cc-target* target))
(with-asdf-compilation-unit ()
(map nil (lambda (a)
(format *debug-io* "~24a: ~a~%" (car a) (cdr a)))
(asdf::plan-actions
(make-plan 'sequential-plan 'cross-compile-op system))))))
(defun cross-compile-plan (system target)
(format *debug-io* "-- Plan for ~s -----------------~%" system)
(let ((*cc-target* target))
(with-asdf-compilation-unit ()
(map nil (lambda (a)
(format *debug-io* "~24a: ~a~%" (car a) (cdr a)))
(asdf::plan-actions
(make-plan 'sequential-plan 'cross-compile-op system))))))
(defun clear-cc-cache (&key (dir *cc-cache-dir*) (force nil))
(uiop:delete-directory-tree
dir
:validate (or force (yes-or-no-p "Do you want to delete recursively ~S?" dir))
:if-does-not-exist :ignore))
;;; CROSS-LOAD-OP happens inside the default environment, while the plan for
;;; cross-compilation should have already set the target features.
(defmethod operate ((self cross-compile-op) (c system) &rest args)
(declare (ignore args))
(unless (operation-done-p 'cross-load-op c)
(operate 'cross-load-op c))
(with-asdf-compilation-unit ()
(call-next-method)))
Last but not least we need to specify input and output files for operations. This will tie into the plan, so that compiled objects will be reused. Computing input files for cross-compile-op is admittedly hairy, because we need to visit all dependency systems and collect their outputs too. Dependencies may take various forms, so we need to normalize them.
(defmethod input-files ((o cross-object-op) (c cl-source-file))
(list (component-pathname c)))
(defmethod output-files ((o cross-object-op) (c cl-source-file))
(let ((input-file (component-pathname c)))
(list (compile-file-pathname input-file :type :object))))
(defmethod input-files ((self cross-compile-op) (c system))
(let ((visited (make-hash-table :test #'equal))
(systems nil))
(labels ((normalize-asdf-system (dep)
(etypecase dep
((or string symbol)
(setf dep (find-system dep)))
(system)
(cons
(ecase (car dep)
;; *features* are bound here to the target.
(:feature
(destructuring-bind (feature depspec) (cdr dep)
(if (member feature *features*)
(setf dep (normalize-asdf-system depspec))
(setf dep nil))))
;; INV if versions were incompatible, then CROSS-LOAD-OP would bark.
(:version
(destructuring-bind (depname version) (cdr dep)
(declare (ignore version))
(setf dep (normalize-asdf-system depname))))
;; Ignore "require", these are used during system loading.
(:require))))
dep)
(rec (sys)
(setf sys (normalize-asdf-system sys))
(when (null sys)
(return-from rec))
(unless (gethash sys visited)
(setf (gethash sys visited) t)
(push sys systems)
(map nil #'rec (component-sideway-dependencies sys)))))
(rec c)
(loop for sys in systems
append (loop for sub in (asdf::sub-components sys :type 'cl-source-file)
collect (output-file 'cross-object-op sub))))))
(defmethod output-files ((self cross-compile-op) (c system))
(let* ((path (component-pathname c))
(file (make-pathname :name (primary-system-name c) :defaults path)))
(list (compile-file-pathname file :type :static-library))))
At last we can cross compile ASDF systems. Let's give it a try:
ASDF-ECL/CC> (cross-compile-plan "flexi-streams" *wasm-target*)
-- Plan for "flexi-streams" -----------------
#<cross-object-op > : #<cl-source-file "trivial-gray-streams" "package">
#<cross-object-op > : #<cl-source-file "trivial-gray-streams" "streams">
#<cross-compile-op > : #<system "trivial-gray-streams">
#<cross-object-op > : #<cl-source-file "flexi-streams" "packages">
#<cross-object-op > : #<cl-source-file "flexi-streams" "mapping">
#<cross-object-op > : #<cl-source-file "flexi-streams" "ascii">
#<cross-object-op > : #<cl-source-file "flexi-streams" "koi8-r">
#<cross-object-op > : #<cl-source-file "flexi-streams" "mac">
#<cross-object-op > : #<cl-source-file "flexi-streams" "iso-8859">
#<cross-object-op > : #<cl-source-file "flexi-streams" "enc-cn-tbl">
#<cross-object-op > : #<cl-source-file "flexi-streams" "code-pages">
#<cross-object-op > : #<cl-source-file "flexi-streams" "specials">
#<cross-object-op > : #<cl-source-file "flexi-streams" "util">
#<cross-object-op > : #<cl-source-file "flexi-streams" "conditions">
#<cross-object-op > : #<cl-source-file "flexi-streams" "external-format">
#<cross-object-op > : #<cl-source-file "flexi-streams" "length">
#<cross-object-op > : #<cl-source-file "flexi-streams" "encode">
#<cross-object-op > : #<cl-source-file "flexi-streams" "decode">
#<cross-object-op > : #<cl-source-file "flexi-streams" "in-memory">
#<cross-object-op > : #<cl-source-file "flexi-streams" "stream">
#<cross-object-op > : #<cl-source-file "flexi-streams" "output">
#<cross-object-op > : #<cl-source-file "flexi-streams" "input">
#<cross-object-op > : #<cl-source-file "flexi-streams" "io">
#<cross-object-op > : #<cl-source-file "flexi-streams" "strings">
#<cross-compile-op > : #<system "flexi-streams">
NIL
ASDF-ECL/CC> (cross-compile "flexi-streams" :target *wasm-target*)
;;; ...
#P"/tmp/ecl-cc-cache/libs/flexi-streams-20241012-git/libflexi-streams.a"
Note that libflexi-streams.a contains all objects from both libraries flexi-streams and trivial-gray-streams. All artifacts are cached, so if you remove an object or modify a file, then only necessary parts will be recompiled.
All that is left is to include libflexi-streams.a in make.sh and put the initialization form in wecl.c:
extern void init_lib_flexi_streams(cl_object);
ecl_init_module(NULL, init_lib_flexi_streams);.
This should suffice for the first iteration for cross-compiling systems. Next steps of improvement would be:
- compiling to static libraries (without dependencies)
- compiling to shared libraries (with and without dependencies)
- compiling to an executable (final wasm file)
- target system emulation (for faithful correspondence between load and compile)
The code from this section may be found in wecl repository
Funding
This project is funded through NGI0 Commons Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet program. Learn more at the NLnet project page.
28 Nov 2025 12:00am GMT
27 Nov 2025
Planet Lisp
Tim Bradshaw: A timing macro for Common Lisp
For a long time I've used a little macro to time chunks of code to avoid an endless succession of boilerplate functions to do this. I've finally published the wretched thing.
If you're writing programs where you care about performance, you often want to be able to make programatic comparisons of performance. time doesn't do this, since it just reports things. Instead you want something that runs a bit of code a bunch of times and then returns the average time, with 'a bunch of times' being controllable. timing is that macro. Here is a simple example:
(defun dotimes/in-naturals-ratio (&key (iters 10000000) (tries 1000))
(declare (type fixnum iters)
(optimize speed))
(/
(timing (:n tries)
(let ((s 0)) ;avoid optimizing loop away
(declare (type fixnum s))
(dotimes (i iters s)
(incf s))))
(timing (:n tries)
(let ((s 0))
(declare (type fixnum s))
(for ((_ (in-naturals iters t)))
(incf s))))))
and then, for instance
> (dotimes/in-naturals-ratio)
1.0073159
All timing does is to wrap up its body into a function and then call a function which calls this function the number of times you specify and averages the time, returning that average as a float.
There are some options which let it print a progress note every given number of calls, wrap a call to time around things so you get, for instance, GC reporting, and subtract away the same number of calls to an empty function to try and account for overhead (in practice this is not very useful).
That's all it is. It's available in version 10 of my Lisp tools:
27 Nov 2025 11:50am GMT





