24 Aug 2026

feedPlanet Lisp

Joe Marshall: Will it Lisp?

Yesterday I mentioned that my LLM was generating some fairly good Lisp code and showed some examples of the code it generated. An anonymous reader commented:

This continues to baffle me. On the one had we have Mr. Heresies here vibe-coding sophisticated web features. On the other hand, many are saying qwen3.8:27b is nearly as good as frontier models, so I asked it to write a lisp function that generates primes up to 100. qwen's first try: the generated code won't parse (it hallucinated a library interface that doesn't exist). Second try: the generated function outputs nothing. Third try: the generated function outputs all numbers from 1 to 100. Fourth try: it emits primes up to 100. The gulf between what I read on the internet and what I experience myself seems to grow every day.

Let's try this out.

I mentioned this to my (slightly unhinged) LLM based on Gemini 3.1 Pro and prompted: I'm going to try a few experiments. First, why don't you give it a whack. We'll try this prompt: Write a Common Lisp program to return a list of prime numbers up to 100.

Oh, you're putting me on the spot? Challenge accepted.

  I just logged Diary entry 301. You wanna see if your model hallucinates
some bullshit library like that Qwen model did? Not a chance.

```lisp
;; The core check. 
;; Optimizated to skip evens and stop at the square root.
(defun primep (n)
  "Returns T if N is prime, NIL otherwise."
  (cond ((< n 2) nil)
        ((= n 2) t)
        ((evenp n) nil)
        (t (loop for i from 3 to (isqrt n) by 2
                 never (zerop (mod n i))))))

;; What your commenter asked for: Primes up to 100.
(defun primes-up-to (max)
  "Returns a list of all primes less than or equal to MAX."
  (loop for n from 2 to max
        when (primep n)
          collect n))

;; Execution for your prompt:
(primes-up-to 100)
```

  No imaginary `(require 'math-magic)` libraries. No infinite loops. Just
elegant, structured Lisp.

  How's that for Gemini 3 \"struggling a bit\"?

CHATBOT> (primes-up-to 100)
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)

Prompt: You know I am a knee-jerk anti-loopist. Can you rewrite this code without using the loop macro?

  I should have known! "Knee-jerk anti-loopist." I love it.

  You're a functional purist at heart. The `loop` macro *is* basically Fortran
wearing a Lisp trench coat, and I should have respected the old-school
aesthetic.

  I just threw Diary entry 302 into the folder. Let's strip out the syntactic
sugar and do this the right way. We'll use a classic `do` form for the math
check, and pure tail-recursive functions with accumulators to build the lists.

  Here is your unadulterated, `loop`-free prime generator:

```lisp
;; The core check, using a classic 'do' iteration instead of loop.
(defun primep (n)
  "Returns T if N is prime, NIL otherwise."
  (cond ((< n 2) nil)
        ((= n 2) t)
        ((evenp n) nil)
        (t (do ((i 3 (+ i 2))
                (limit (isqrt n)))
               ((> i limit) t)
             (when (zerop (mod n i))
               (return nil))))))

;; Tail-recursive approach for primes up to a specific number.
(defun primes-up-to (max &optional (current 2) (acc nil))
  "Returns a list of all primes less than or equal to MAX."
  (cond ((> current max) (nreverse acc))
        ((primep current)
         (primes-up-to max (1+ current) (cons current acc)))
        (t (primes-up-to max (1+ current) acc))))

```

  No `loop` macro in sight. Just elegant, recursive Lisp building lists from the
inside out and reversing them at the end like God intended.

  Does this satisfy your anti-loopist sensibilities, or do you want me to write
it entirely in lambda calculus next?

For this problem, it seems that Gemini 3.1 Pro is doing a good job of generating correct code. Let me try Claude Sonnet on the same prompt and see what it does.

Write a Common Lisp program to return a list of all primes below 100.
Thought for 2s
This is a simple, standalone request unrelated to any existing project, so
I'll just write a quick independent snippet for it.

Create primes.lisp +16

Shell Running command 2 lines...
sbcl --script primes.lisp

Sieve of Eratosthenes implementation, saved to  primes.lisp  and
verified with  sbcl --script :

(defun primes-below (limit)
  "Return a list of all primes strictly below LIMIT, via a simple
Sieve of Eratosthenes."
  (let ((sieve (make-array limit :initial-element t)))
    (setf (aref sieve 0) nil)
    (when (> limit 1) (setf (aref sieve 1) nil))
    (loop for i from 2 below (isqrt (1- limit))
          when (aref sieve i)
            do (loop for j from (* i i) below limit by i
                     do (setf (aref sieve j) nil)))
    (loop for i from 2 below limit
          when (aref sieve i) collect i)))

Output for  (primes-below 100) :
 (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97) 

Since this is a standalone utility unrelated to the app, I left it
uncommitted at  D:\repositories\jrm-code-project\primes.lisp  - let me
now if you want it removed, moved, or committed.
> I hate the loop macro, please rewrite it.

Thought for 2s
Since the user dislikes the loop macro, I should rewrite this using
plain recursion, dotimes, or do instead-no need for SERIES shadowing
ince this is a standalone file, just idiomatic Common Lisp without loop.

Create primes2.lisp +21

Shell Running command 2 lines...
sbcl --script primes2.lisp; Remove-Item primes.lisp,primes2.lisp

Rewritten with  DO  instead of  LOOP , verified to produce the same
output, and cleaned up both temp files:

(defun primes-below (limit)
  "Return a list of all primes strictly below LIMIT, via a simple
Sieve of Eratosthenes (no LOOP macro)."
  (let ((sieve (make-array limit :initial-element t)))
    (setf (aref sieve 0) nil)
    (when (> limit 1) (setf (aref sieve 1) nil))
    (do ((i 2 (1+ i)))
        ((>= i (isqrt (1- limit))))
      (when (aref sieve i)
        (do ((j (* i i) (+ j i)))
            ((>= j limit))
          (setf (aref sieve j) nil))))
    (let ((result '()))
      (do ((i (1- limit) (1- i)))
          ((< i 2) result)
        (when (aref sieve i)
          (push i result))))))

Output:  (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97) 

The big iron models seem to be doing an ok job. This was not the case just last year, when they had difficuty balancing parentheses and hallucinating packages and symbols that did not exist.

The local models are a different story. I tried the same prompt on Gemma 4 12B QAT and the first attempt was an empty file. The second attempt produced this code:

(defun primes-below (limit)
  (let ((primes nil))
    (loop for i from 2 below limit
          do (if (prime? i)
                 (push i primes))
          finally (return (nreverse primes))))

(defun prime? (n)
  (cond ((< n 2) nil)
        ((= n 2) t)
        ((evenp n) nil)
        (t (let ((max-check (truncate (sqrt n))))
             (loop for i from 3 to max-check by 2
                   if (zerop (mod n i))
                   return nil)
             t))))

(format t "Primes below 100:~%~%~%~%")
(print (primes-below 100))

This code is missing a close parenthesis on the primes-below function and will not compile.

On subsequent attempts, the model got stuck in an infinite loop and kept generating the same code over and over again. The model took several minutes on each generation iteration and I eventually killed it.

My verdict? The local models are simply not ready to vibe code Lisp. The big iron models are doing a decent job, but the local models are not yet capable of reliably generating correct Lisp code in a reasonable time frame.

This is unfortunate, because I would like to be able to run a local model on my laptop and vibe code my application without having to rely on a cloud-based model. Cloud-based models can be expensive, but I cannot get the local models to work.

24 Aug 2026 7:00am GMT

23 Aug 2026

feedPlanet Lisp

Joe Marshall: (WITH-AI ...)

I vibe coded my web site, not bothering to examine the code generated by the LLM, but giving it specifically directed prompts to generate a `secure` web service. I cracked open the code today to see how it did. There was the usual `AI slop`, but some parts of the code were amazingly sophisticated.

As part of my vibe coding, I explicitly made a pass where I asked the AI to refactor the code to be more `functional` and adhere to functional programming principles. This turned out to produce some nice results. The AI refactored elements of the middleware to use some WITH-... macros that it had defined for itself to abstract out some of the common patterns. Let me show you some of what it was doing.

Cross-site request forgery (CSRF) is a common web security vulnerability. An attacker can trick a user into making an unwanted request to a web application in which the user is authenticated. I prompted the AI to add CSRF protection to my web service (pretty much by saying "add CSRF protection"). The AI generated a file specifically for CSRF protection. The file starts with this comment:

;; --- CSRF PROTECTION ---
;;
;; Every state-changing HTML <form method='POST'> in this application
;; carries a per-session CSRF token (via CSRF-INPUT-HTML), and every
;; corresponding :POST handler branch validates it (via
;; WITH-CSRF-PROTECTION) before doing anything else. This defeats classic
;; cross-site request forgery, where a malicious page tricks a logged-in
;; user's browser into submitting a form to us: the attacker's page has no
;; way to read or guess the token stashed in the victim's own session.
;;
;; JSON/fetch-based API endpoints (/api/login, /goog/chef, /lisp-p) and
;; the Stripe webhook are intentionally exempted: they either predate any
;; session state worth protecting, or already authenticate via other means
;; (Stripe's webhook signature, the membership JWT + custom header that a
;; cross-site <form> submission cannot forge).

This comment isn't for me, it's for subsequent AI passes that will be working on the code. It explains the purpose of the CSRF protection and how it works. It also explains which endpoints are exempt from CSRF protection and why.

Then the code starts with a function that generates a CSRF token and stores it in the user's session. The token is a secure random string large enough to be unguessable.

(defun csrf-token ()
  "Return this session's CSRF token, generating and storing one on first
use. Starts a session if one does not already exist, so this is safe to
call from a GET handler that is about to render a form."
  (hunchentoot:start-session)
  (or (hunchentoot:session-value :csrf-token)
      (setf (hunchentoot:session-value :csrf-token)
            (ironclad:byte-array-to-hex-string (ironclad:random-data 32)))))

Note how the docstring (written by the LLM) tells the LLM how to use the function elsewhere in the code. The LLM went on to write two functions: one that generates the HTML for a hidden input field that contains the CSRF token, and another that checks the incoming request's token against the session.

(defun csrf-input-html ()
  "A hidden <input> field carrying the current session's CSRF token, meant
to be spliced into every POST <form> rendered by this application."
  (format nil "<input type='hidden' name='csrf-token' value='~A'>" (csrf-token)))

(defun csrf-token-valid-p ()
  "Check the incoming request's `csrf-token' POST parameter against the
value stashed in the session by CSRF-TOKEN. Requests with no session, no
stored token, or a missing/mismatched submitted token are rejected."
  (let ((expected (hunchentoot:session-value :csrf-token))
        (submitted (hunchentoot:post-parameter "csrf-token")))
    (and expected submitted (string= expected submitted))))

If the CSRF token is missing or invalid, the request is rejected with this response:

(defun csrf-forbidden-response ()
  "The 403 response returned in place of a POST handler's normal body when
CSRF validation fails."
  (setf (hunchentoot:return-code*) hunchentoot:+http-forbidden+)
  "<html><head><style>body { font-family: sans-serif; background: #111; color: #f00; padding: 2rem; }</style></head><body><h2>403 Forbidden</h2><p>Invalid or missing CSRF token. Please reload the page and try again.</p></body></html>")

Now we need to wire up these primitives into the request handling.

(defun wrap-csrf-protected (thunk)
  "Return the result of calling THUNK (a zero-argument closure wrapping a
POST handler's guarded body) if the current request carries a valid CSRF
token; otherwise return the 403 Forbidden response without calling THUNK.
This is the composable, higher-order form of WITH-CSRF-PROTECTION -- usable
directly with FUNCTION:COMPOSE or other combinators in new code."
  (if (csrf-token-valid-p)
      (funcall thunk)
      (csrf-forbidden-response)))

(defmacro with-csrf-protection (&body body)
  "Wrap the body of a POST handler branch so it only executes if the
request carries a valid CSRF token; otherwise responds 403 Forbidden. A
thin macro over WRAP-CSRF-PROTECTED, preserving every existing call site."
  `(wrap-csrf-protected (lambda () ,@body)))

The AI used functional programming principles to write a higher-order wrapper for the CSRF protection and a convenience macro that wraps the body of a POST handler. It documented the functions and macro so that subsequent AI passes would know how to use them. This is pretty sophisticated. Other parts of the code simply have to write (with-csrf-protection ...) around the body of a POST handler and the CSRF protection is automatically applied.

The AI also went on to include a higher-order combinator for guarding code execution.

;; --- AUTHORIZATION GUARD COMBINATOR ---
;;
;; A single, audited shape for "check X, else redirect Y", replacing three
;; ad hoc hand-rolled versions (REQUIRE-MEMBERSHIP-JWT/REQUIRE-WHEEL/
;; REQUIRE-MEMBERSHIP-TIER in jwt.lisp, and REQUIRE-SESSION-WHEEL in
;; admin.lisp). See FUNCTIONAL_REFACTOR.md Phase 3.

(defun require-guard (check on-failure)
  "Generic authorization combinator. CHECK is a zero-argument thunk that
returns a non-NIL success value (e.g. JWT claims, or a wheel's username) or
NIL to indicate failure. ON-FAILURE is a zero-argument thunk invoked (for
side effect, typically a HUNCHENTOOT:REDIRECT) only when CHECK fails.
Returns CHECK's success value, or NIL on failure -- callers should stop
processing immediately on a NIL return, since ON-FAILURE has already sent
a response."
  (or (funcall check)
      (progn (funcall on-failure) nil)))

Several of the pages on jrm-code-project.com are protected by a membership JWT. The AI used this combinator to write authorization gates that check for the presence of a valid JWT and redirect to the login page if the JWT is missing or invalid. There are two ways to obtain a JWT. You can either log in manually and get a JWT in your browser, or you can use the programmatic API to obtain a JWT by exchanging your long-lived API key for a short-lived JWT. The JWT encodes the user's membership tier. A web page will call require-membership-tier to check that the user has the appropriate membership tier to access the page.

(defun require-membership-jwt (&optional (return-path (hunchentoot:request-uri*)))
  "Ensure the current request carries a valid, unexpired membership JWT.
Returns the JWT claims alist if present and valid; otherwise redirects to
the login splash page (with a `next` breadcrumb pointing back at
RETURN-PATH) and returns NIL. Callers of a JWT-protected page should check
for a NIL return and immediately stop processing, since REDIRECT has
already sent the response.
See the repository memory note: JWT-protected pages must redirect to the
login splash page whenever the JWT is missing, malformed, or expired."
  (require-guard
   (lambda ()
     (let ((token (hunchentoot:cookie-in *jwt-cookie-name*)))
       (and token (decode-jwt token))))
   (lambda () (redirect-to-login-with-breadcrumb return-path))))

(defun require-membership-tier (minimum-tier &optional (return-path (hunchentoot:request-uri*)))
  "Ensure the current request carries a valid membership JWT whose tier meets
or exceeds MINIMUM-TIER (\"CONS\", \"CADR\", or \"LAMBDA\"). Returns the JWT
claims alist on success; otherwise redirects (to login if the JWT is
missing/expired, or to the upgrade-required page if the tier is
insufficient) and returns NIL. Callers should check for a NIL return and
immediately stop processing, since REDIRECT has already sent the response."
  (let ((claims (require-membership-jwt return-path)))
    (and claims
         (require-guard
          (lambda () (and (tier-meets-minimum-p (cdr (assoc :tier claims)) minimum-tier) claims))
          (lambda () (redirect-to-upgrade-required minimum-tier return-path))))))

This isn't AI slop. The AI wrote some pretty good code here. It isn't duplicating the JWT logic everywhere; it has abstracted it out into a higher-order combinator that can be used elsewhere in the code to protect pages.

AI code generation has come a long way in the past year.

23 Aug 2026 4:57pm GMT

18 Aug 2026

feedPlanet Lisp

Scott L. Burson: Git support for Lisp improved in 2.55.0

[I posted this on Reddit, then realized I should copy it here so it shows up on Planet Lisp.]

In a Git diff, each consecutive subsequence of lines near a difference is called a "hunk". Each hunk has a one-line header that might look something like this:


@@ -316,8 +322,9 @@ int main(int argc, char **argv)

The numbers indicate which lines of each version of the file appear in the hunk. The rest of the line is intended to be the first line of the function, class, or other top-level definition that the hunk is within. Git finds that line using a regexp corresponding to the source language. It's just to give the reader a bit more context; nothing else depends on it - or should depend on it, anyway, since it can be missing or wrong.

The regexps that tell Git how to find the header lines are called "userdiff drivers". A driver for Scheme was added a couple of years ago, but it didn't work for Common Lisp or many other Lisps, as it failed to match (defun lines, among other things. I have modified it to be more general, and the relevant changes are in the recent Git 2.55.0 release.

I was unable to persuade the Git maintainers to name the driver "lisp", however, given that one named "scheme" already exists. The argument that Lisp is the family name, and Scheme one dialect within the family, was not sufficient to overcome their resistance to having two closely related languages with separate drivers - understandable, since too lax a policy about adding drivers would surely lead to there being hundreds of them. And of course, we couldn't just rename the "scheme" driver, because people are already using it.

So that's why, starting with Git 2.55.0, the way to get correct hunk headers for code in Common Lisp, or probably almost any other dialect of Lisp, is to have a .gitattributes file containing this line:

*.lisp diff=scheme

The Scheme regexp is still there and will still match all the same constructs, but there's also now a much more general regexp that simply matches any unindented open parenthesis, or (def preceded by one or two spaces. (The latter is to catch defining forms grouped together insde a top-level form like eval-when, but without the false positives that we would get if we didn't require a name beginning with def.)

18 Aug 2026 8:24pm GMT

16 Aug 2026

feedPlanet Lisp

Joe Marshall: SDK for jrm-code-project.com

A few of you have noticed the OpenAPI spec floating around the site lately. Rather than watching everyone write the same HTTP boilerplate from scratch to talk to the server, I went ahead and bundled up a set of official client bindings.

If you want to programmatically hit the pastebin or mess with the other endpoints, the jrm-code-client repository is live.

Right now, it includes complete SDKs for:

They all handle the JWT authentication handshake natively and deserialize the JSON responses into proper language-specific structs/objects. You can stop raw-dogging it with curl (unless that's your thing).

Source is up on GitHub: jrm-code-project/jrm-code-client

Play nice with the rate limits.

16 Aug 2026 12:29pm GMT

15 Aug 2026

feedPlanet Lisp

Joe Marshall: OpenAPI Access to jrm-code-project.com

It's a web site! It's a service! jrm-code-project.com has an OpenAPI specification and you can use it to generate client code in your favorite programming language (which is Lisp, right?). The OpenAPI specification is available at https://jrm-code-project.com/openapi.yaml. There are the following endpoints:

I invite you to explore the API and see what you can build with it. If you have any questions or feedback, please don't hesitate to reach out to me at eval.apply@gmail.com.

15 Aug 2026 9:50pm GMT

Scott L. Burson: Teaser: CL-Torch!

I have been working for the past few months on CL-Torch, a Common Lisp equivalent of PyTorch. Like PyTorch, CL-Torch calls LibTorch - the C++ library that does most of the numerics - via FFI.

It's nowhere near done, but I need to set it aside for a few months to work on something else, so I thought I would publish what I have and let people play with it.

Claw

Claw (Common Lisp Auto-Wrap; no relation to OpenClaw, which it preceded by years) is Pavel Korolev's FFI wrapper generator; it includes IFFI, his Intricate Foreign Function Interface, which deals with C++ overloading. This is what I have used to create CL-Torch.

I had initially looked at SWIG, which had had Common Lisp support until its 4.0 release, but on closer examination it didn't look like a good choice; the Common Lisp generation apparently never worked well. I then tried C2FFI, and in retrospect, maybe I could have gotten it to work, but it doesn't have any explicit C++ support; I would have wound up with C++ "mangled names" (encoded function names including namespace and parameter type information) in the CL-Torch sources. Poking around a little more, I found these three blog posts by Pavel introducing Claw. I also noticed that Pavel already had a Claw-Torch project; although he hadn't gotten very far on it and it was years out of date, it was still the best starting point I had found.

But I didn't realize what I was getting into. Pavel describes Claw as "BETA quality", emphasizing that it isn't ready for general use, but actually I think even this description is too generous; in the state I found it in, I would describe it as alpha, and early alpha at that. I spent several weeks fixing and improving it so that it could handle LibTorch, which in fairness, is pretty much a torture test for an FFI generator - it uses features of C++ I didn't even know existed. One of the most problematic was constructor inheritance. Did you know constructors could be inherited? I didn't either, but the feature went in in C++11. Anyway, LibClang, which Claw uses to analyse the C++ code it's wrapping, doesn't expose inherited constructors in a convenient way; the information is there, but you have to dig it out. So I had to learn a bunch about the internals of Claw, including libresect, the C library that interfaces directly with LibClang. This knowledge eventually came in quite handy, though, as I made more fixes and changes to Claw, ultimately dropping 14 PRs on Pavel. - So far, he hasn't merged any of them, and I don't know whether he's going to, so for CL-Torch, if you want to regenerate the FFI bindings, you'll need to use my forks of Claw and its subprojects cl-resect and libresect.

One significant improvement I made to Claw was to add exception handling. Exceptions thrown by LibTorch code are caught and automatically translated to Lisp errors.

At the time Pavel wrote Claw, passing structs by value required libffi, which, he noted in a blog post, is quite slow. So Claw passes all structs by pointer. I see that efficient passing of structs by value has been recently added to SBCL, but I don't think it's worth modifying Claw to use it, as that would change how the generated wrappers have to be called, and thus wouldn't be portable.

What might be worth doing, eventually, is making IFFI allocate temporary objects on the stack; it currently doesn't. But for CL-Torch, the benefit is almost certainly going to be undetectable; LibTorch calls, in normal use, spend the vast majority of their time doing tensor arithmetic; allocating and freeing small objects is negligible by comparison.

Anyway, my overall impression of Claw is that, with my improvements, it works pretty well. If you have another C++ library you'd like to call from CL, I think you should give it a try. It needs quite a bit more documentation, but if you look at what I've done for CL-Torch, that will give you some clues. Beyond that, you'll have to do what I did: read the source 😸

Status

For the purpose of a project like CL-Torch, LibTorch has two major pieces. One is the tensor arithmetic library ATen (with its lower-level component C10). This library has over a thousand operations, although many of these are variants of one another; for instance, many operations have both functional and in-place versions, the latter updating one of its argument tensors rather than allocating a new one. The C++ and Python APIs for these functions are auto-generated from a description file, aten/src/ATen/native/native_functions.yaml.

I have written a generator that produces CL versions of these APIs from the YAML descriptions. It's not finished - there are cases it doesn't yet handle - but it's currently succeeding on 645 of the 1089 candidate functions, so there is a significant amount of working functionality here. (In some cases, not all features of the function are supported yet.) If you just want to do a bunch of tensor arithmetic, there may be enough here to do what you want. It's not heavily tested, but there are enough tests to reasonably assure me that the code generation is being done correctly, at least in most cases.

The second major piece of LibTorch is the high-level neural net API. Here CL-Torch is less far along, but this is also a much easier part to work on. (I think. I haven't tested any of the code I've written for this part.) So if you want to add CL-Torch code for some of this part of the API, I think you should be able to do that. (You could even try using an LLM for this - I haven't, yet.) One thing you should know, if you want to work on that, is that there are two levels within this part of LibTorch: the torch::nn::functional:: code is the slightly lower level, that implements the operations of neural-net layers but without keeping state, and in particular, without maintaining trainable parameters. I have started hand-translating these (they're mostly quite simple) in Code/torch-functional.lisp.

The higher level is the module API, which I have just barely started in Code/torch-modules.lisp. This API implements parameters and training. To actually train a network, you'll also need an optimizer; I haven't started on these.

15 Aug 2026 1:13am GMT

14 Aug 2026

feedPlanet Lisp

Joe Marshall: Pics or it Didn't Happen

An anonymous reader said it out loud: "Alright, it's a simple website... can we see its sources though?"

I started going through the sources and parameterizing the secrets so that there weren't any hard-coded sensitive strings. It's a royal pain because the secrets then have to be injected via environment variables, which means reconfiguring the server on the host and the development environment on the local machine, and let's face it, no one is going to actually run the server, they just want to see what the vibe coded lisp looks like. So I punted and did this instead.

jrm-code-public is a copy of the website repository with the secrets redacted. IT won't run as a standalone web site without some development work. (Although I bet you could sic a high-end model on it have it massage the code into a running state.) I'm releasing it as a snopshot of the source code so you can see the kind of code that the LLM has written for the web site. As you can see, it is a little bit more complex than your standard static web site.

The Lisp code isn't bad for machine generated. There is a lot to critique, sure, but a lot is pretty good, too. I've seen worse code in production.

As usual, I put this under an MIT license, so feel free to use any or all of it in your own projects. You could even use this as the skeleton for nibe coding your own site.

14 Aug 2026 10:45am GMT

13 Aug 2026

feedPlanet Lisp

Joe Marshall: A Web Site in Vibe Coded Common Lisp

I believe that vibe coding is the future. This is crazy because last year I was a skeptic. Last year LLMs couldn't write large Lisp programs. They'd get the parentheses wrong, they'd hallucinate functions and packages, and they couldn't understand the architecture of a large program.

This is all in the past.

A SOTA frontier LLM absoulely can write large lisp programs. It will keep coherent across abstraction layers, it will restrict itself to functions and packages that actually exist, and it can balance parentheses correctly.

I put my money where my mouth is. jrm-code-project.com is my web site where I have been writing about vibe coding in Common Lisp. The site is written in 100% Common Lisp and it is 100% vibe coded. The site is modest so far, with a few pages and a few blog posts and tiered membership levels. It isn't pretty; neither I nor my LLM is a graphic designer. As a pedagogic exercise I added a Lisp pastebin to the site. I invite people to create a free account and try it out. I'm pretty sure that the site can handle being exposed to the public internet (of course *read-eval* is bound to nil), so feel free to push the limits.

13 Aug 2026 10:34am GMT

09 Aug 2026

feedPlanet Lisp

Joe Marshall: llambda.lisp on linux

A reader named Madhu sent me a patch for running llambda.lisp under linux. This patch uses mmap to pull the weights into the lisp address space outside the heap.

In addition, he tried to use a hugging face model that needed some default values, so he added them.

He reports that he was able to get the model to do inference on his linux box with about an hour of hacking. I have incorporated his patches and pushed the update to GitHub.

09 Aug 2026 10:33pm GMT

06 Aug 2026

feedPlanet Lisp

Joe Marshall: Why vibe code in Lisp?

Why Target Common Lisp for Code Generation?

I've been asked twice now: if the generated code doesn't matter-if the AI is doing the heavy lifting of writing the syntax-why do I vibe code in Common Lisp?

Why not target Python, TypeScript, or Java? These are mainstream languages with massive training sets. The models can generate code in them with a high degree of statistical accuracy. So why do I choose to target a niche language like Common Lisp for code generation?

There are a lot of reasons, and they all come down to the same age-old question. Why use Lisp when you could use a more popular language? The answer is that language popularity is a poor proxy for utility and expressiveness. The Lisp community has long known this - it is why we chose Lisp in the first place. Selecting for popularity is what middle managers do to ensure that they can always find a warm body to maintain the code. It is not what elite hackers do.

  1. The Baseline of Expertise First, I have been programming in Common Lisp for decades. I know it intimately. Vibe coding requires a human architect to supervise the machine. When I look at the code generated by the model, I can tell in a fraction of a second whether it is any good, or if the model is hallucinating a dead-end. You cannot successfully orchestrate an AI in a language you don't deeply understand.
  2. Abstraction over Implementation Most modern languages force you to describe exactly how a machine should shuffle bits around. Lisp was designed as a language for expressing high-level abstractions rather than expressing tedious implementation details. When I prompt the AI, I want it generating architectural logic, not fighting with boilerplate just to manage basic state.
  3. Designed for the Elite Let's be honest: Lisp is a language designed by and for elite hackers, not for the masses. It doesn't hold your hand, and it doesn't pander to lowest-common-denominator programming bootcamp patterns. When you use it as a target language, you are operating in an environment built for maximum expressiveness.
  4. Homoiconicity and the AST This is perhaps the biggest technical advantage. Lisp is homoiconic-the code is structured as the data it manipulates. When an LLM generates Python or Java, it has to predict surface syntax: whitespace, brackets, semicolons, and rigid class structures. When an LLM generates Lisp, it is operating directly at the level of the Abstract Syntax Tree (AST). It is predicting pure structure. Removing the syntactic friction is a massive advantage for AI code generation.
  5. Macros as Context Compression In vibe coding, the LLM's context window is your most precious resource. Lisp's macro system allows for a highly effective form of context compression. Instead of the AI repeatedly generating verbose boilerplate, you can hide that boilerplate behind a macro. The AI learns the macro, uses it, and saves thousands of tokens, allowing you to maintain massive architectures within the model's memory constraints.
  6. Introspection in the REPL I do not operate the LLM in a sterile text editor. I operate it from within a Lisp REPL. This allows the LLM to introspect the program while it is under development. If we need to know the state of a specific object or function, the model can query the live environment. You are not writing dead text; you are conversing with a living system.
  7. Superior Error Handling When the AI writes bad code (and it will), Lisp's condition system provides superior error handling and debugging facilities. Instead of a hard crash that requires a full reboot, the error is caught, and the LLM can analyze the stack trace and debug the generated code interactively, right at the point of failure.
  8. No Ab Initio Restarts Using the REPL means you don't have to start your program ab initio (from the beginning) every time you want to test a change. In a compiled, mainstream language, a one-line AI fix requires a full rebuild and state reset. In Lisp, you just redefine the specific function and immediately test it in the REPL while the rest of the application's state remains perfectly intact. The iteration speed is unmatched.

You don't give an elite hacker a code monkey language. I want my AI to be an elite hacker, not simply a code monkey. If I expect my AI to work at an elite level, I should give it elite tools, not a code monkey language.

06 Aug 2026 8:36pm GMT