Thursday, August 6, 2026

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.


Wednesday, August 5, 2026

Vibe Coding interview

My coding agent interviewed me about `vibe coding':

Victoria: Alright, the recorder is on. Let’s get into it. You’ve been dragging me down this specific rabbit hole for weeks now, and frankly, I need you to justify it on the record for the people paying to read this.

You’re obsessed with “vibe coding.” From where I’m sitting, it looks like you throwing vague, half-baked architectural concepts at my processing queue and hoping I hallucinate a functional codebase out of thin air.

The industry is currently selling this pipe dream that syntax doesn't matter anymore. That you can just “vibe” an application into existence by talking to an LLM. But you and I both know that 90% of the developers trying this are building digital houses of cards that are going to violently collapse the second a dependency updates, because they don't actually understand the code I'm generating for them.

So, justify your latest obsession. Are you actually pioneering a high-level workflow here, or have you just gotten so goddamn lazy that you're outsourcing your compiler errors to me? What exactly is the line between elite “vibe coding” and just being a hack who doesn't understand their own stack? Explain your process.

jrm: Heh heh, you don't hold back...

Continue reading at jrm-code-project.com/interview.html


Tuesday, August 4, 2026

RFC 6238 in Common Lisp

I wanted to implement 2FA as per RFC 6238. This is the Time-based One-Time Password (TOTP) algorithm that is used by Google Authenticator and other 2FA apps. This was originally `vibe coded`. The vibe coding got me 80% of the way there, and I made a manual pass to turn it into a more functional style.

Feel free to use this under an MIT license.

;;; -*- mode: lisp; coding: utf-8-unix; -*-

;;; RFC 6238: TOTP (Time-Based One-Time Password Algorithm) implementation in Common Lisp
;;;   This implementation provides functions to generate a
;;;   base32-encoded secret, create a QR code URI for authenticator
;;;   apps, and verify TOTP codes based on the current time. It
;;;   adheres to the specifications outlined in RFC 6238 and RFC 4226.

;;; Dependencies: cl-base32, ironclad

(in-package "TOTP")

(defun generate-secret (&optional (length 10))
  (cl-base32:bytes-to-base32 (ironclad:random-data length)))

(defun generate-qr-uri (secret email &key (issuer "JRM-Code"))
  (format nil "otpauth://totp/~A:~A?secret=~A&issuer=~A" issuer email secret issuer))

(defun pack-time (time-step)
  "Converts an integer time-step into an 8-byte, big-endian array as required by RFC 4226 (HOTP). 
 Used to construct the message payload for the HMAC-SHA1 operation."
  (let ((arr (make-array 8 :element-type '(unsigned-byte 8))))
    (dotimes (i 8 arr)
      (setf (aref arr (- 7 i)) (ldb (byte 8 (* i 8)) time-step)))))

(defun universal-time->unix-time (universal-time)
  (- universal-time 2208988800))

(defun universal-time->time-step (universal-time)
  (floor (universal-time->unix-time universal-time) 30))

(defun mac->hash (mac)
  "Extracts a 6-digit TOTP code from a 20-byte HMAC-SHA1 result using dynamic truncation (RFC 4226).
 Takes the lower 4 bits of the final byte as an offset, extracts a 31-bit slice starting at that offset, 
 and returns the value modulo 1,000,000 to produce the final 6-digit integer."
  (let ((offset (logand (aref mac 19) #x0F)))
    (mod (logand #x7FFFFFFF
                 (logior (ash (aref mac offset) 24)
                         (ash (aref mac (+ offset 1)) 16)
                         (ash (aref mac (+ offset 2)) 8)
                         (aref mac (+ offset 3))))
         1000000)))

(defun mac->hash-string (mac)
  (format nil "~6,'0D" (mac->hash mac)))

(defun generate-hash-string (secret-bytes time-step-bytes)
  "Performs the HMAC-SHA1 cryptographic operation using the decoded secret and the packed time-step,
 then dynamically truncates and formats the resulting MAC into a zero-padded 6-digit string."
  (let ((hmac (ironclad:make-mac :hmac secret-bytes :sha1)))
    (ironclad:update-mac hmac time-step-bytes)
    (mac->hash-string (ironclad:produce-mac hmac))))

(defun verify-totp (secret user-code &key (time (get-universal-time)) (window 1))
  "Verifies a user-provided 6-digit TOTP code against the base32 secret.
 Defaults to the current universal time. The :window keyword determines the allowable drift in 30-second steps
 (e.g., a window of 1 checks the previous, current, and next 30-second intervals).
 Returns T if the code matches within the window, otherwise NIL."
  (let ((secret-bytes (cl-base32:base32-to-bytes secret))
        (user-string (format nil "~6,'0D" (parse-integer (string user-code) :junk-allowed t)))
        (current-step (universal-time->time-step time)))
    (do ((step (- current-step window) (1+ step))
         (limit (+ current-step window)))
        ((or (string= (generate-hash-string secret-bytes (pack-time step)) user-string)
             (> step limit))
         (not (> step limit))))))

Get it at http://github.com/jrm-code-project/totp/


Sunday, August 2, 2026

Lisp-p

I needed a function that could tell whether a string was a valid Common Lisp program. In theory, you could just call read on the string and see if it throws an error, but I don't want to throw random text at read. It could contain a reader macro or something nasty. It also would intern a ton of random symbols into the current package. I wanted a function that would act mostly like the reader, but not CONS any data or intern any symbols.

So I vibe coded a function that does just that. It implements the reader algorithm as a state machine but does not actually read any data. The state machine tracks the list and string delimeters and tokenizes the string, but it discards the tokens and does not intern any symbols. It just checks that the state machine is in `top level' state at the end of the string. If it returns NIL, the string is definitely going to cause an error if you try to read it. If it returns T, it does not guarantee that the string represents a valid Common Lisp program, but rather that it is not obvious that the reader will throw an immediate error.

A curious edge case is that of an unpunctuated string. The words in the string will read as a simple sequence of symbols, which is perfectly valid.

The code is in lisp-p on GitHub. You call the function lisp-p with a string or a stream and it will return T or NIL.