Monday, August 24, 2026

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.


No comments: