Skip to content
Volume L · The TensorCore.si Library

Lisp

The original AI language.

Lisp, created by John McCarthy in 1958, is the second-oldest high-level language still in use, and it was the language of AI research for decades. It introduced garbage collection, recursion as a core tool, and code written as data. This volume uses Common Lisp, the main standard dialect.

Prologue

Why learn Lisp?

Lisp shaped modern programming and early AI, and its macros still show what a programmable language can be.

  • AI heritage: expert systems and early AI research were written in Lisp.
  • Code as data: macros let you extend the language itself.
  • Interactive: redefine functions while the program runs.
Prologue

Set up and run your first program

Install SBCL (Steel Bank Common Lisp) and Quicklisp for libraries. Emacs with SLIME or VS Code with Alive make a great environment.

terminal
sudo apt install sbcl
sbcl --script hello.lisp
hello.lisp
(defparameter *name* "Ada")
(format t "Hello, AI! Welcome to Lisp, ~a.~%" *name*)
Tipformat uses directives: ~a prints a value and ~% prints a newline.
Chapter 1

S-expressions and values

Code and data share one form: lists in parentheses. The first element is the function. Global variables are named with *earmuffs* by convention.

basics.lisp
(defparameter *layers* 32)
(defparameter *model* "llama3.1")
(format t "~a: ~a layers~%" *model* *layers*)
(format t "FP16: ~,1f GB~%" (/ (* 8.03e9 2) 1e9))
(print (list 1 2 3))
(print '(this is data, not code))
Chapter 2

Functions

defun defines functions. Optional and keyword arguments make flexible interfaces. lambda makes anonymous functions.

functions.lisp
(defun params (layers &optional (width 4096))
  (* 12 layers width width))

(defun square (x) (* x x))

(print (params 32))
(print (mapcar #'square '(1 2 3 4)))
(print (funcall (lambda (x) (* 2 x)) 21))
(print (reduce #'+ '(80 141 180)))
Chapter 3

Lists and recursion

Lists are built from cons cells. car gets the first item and cdr the rest; recursion over lists is natural.

lists.lisp
(defparameter *vram* '(("H100" . 80) ("H200" . 141) ("RTX 5090" . 32)))

(defun big-gpus (gpus)
  (cond ((null gpus) nil)
        ((>= (cdar gpus) 80) (cons (caar gpus) (big-gpus (cdr gpus))))
        (t (big-gpus (cdr gpus)))))

(print (big-gpus *vram*))                     ; ("H100" "H200")
(print (cdr (assoc "H200" *vram* :test #'string=)))
Chapter 4

Control flow and loop

if, when, cond and case make decisions. The loop macro is a mini-language for iteration.

control.lisp
(defun precision (bits)
  (case bits (32 "FP32") (16 "FP16") (8 "FP8") (t "other")))
(print (precision 8))

(loop for epoch from 1 to 3 do (format t "epoch ~a~%" epoch))
(print (loop for x in '(1 2 3 4 5) when (evenp x) collect (* x x)))
(print (loop for i from 1 to 10 sum i))
Chapter 5

Macros: code that writes code

Because code is a list, macros can transform it before it runs. This is how Lisp programmers add new syntax to the language.

macros.lisp
(defmacro with-timing (&body body)
  `(let ((start (get-internal-real-time)))
     (prog1 (progn ,@body)
       (format t "took ~a ms~%"
               (round (* 1000 (- (get-internal-real-time) start))
                      internal-time-units-per-second)))))

(with-timing
  (loop for i from 1 to 1000000 sum i))
Part II · AI with Lisp

Lisp and AI

Lisp was the language of symbolic AI: McCarthy coined the term "artificial intelligence", and expert systems, early natural language programs and Lisp machines all came from this world. Today's deep learning mostly uses Python, but Lisp is still used for symbolic reasoning, planning and research, and it can call LLM APIs with libraries like Dexador.

The essential libraries

DexadorHTTP client for API calls.
jzon / yasonJSON parsing and writing.
MGLMachine learning library for Common Lisp.
py4cl2Call Python libraries from Lisp.

A tiny symbolic rule engine

Classic symbolic AI: facts plus if-then rules, applied until nothing new is learned.

rules.lisp
(defparameter *facts* '((gpu h100) (memory h100 80)))
(defparameter *rules*
  '(((gpu ?x) (memory ?x 80)) . (datacenter ?x)))

(defun infer ()
  (dolist (f *facts*)
    (when (eq (first f) 'gpu)
      (let ((x (second f)))
        (when (member (list 'memory x 80) *facts* :test #'equal)
          (pushnew (list 'datacenter x) *facts* :test #'equal)))))
  *facts*)

(print (infer))   ; adds (DATACENTER H100)
Part III · Build an AI agent

Build an AI agent in Lisp

With Dexador for HTTP and jzon for JSON (load both with Quicklisp), Common Lisp runs the same tool-calling loop as the other volumes.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.lisp
(ql:quickload '(:dexador :com.inuoe.jzon))
(defpackage :agent (:use :cl) (:local-nicknames (:jzon :com.inuoe.jzon)))
(in-package :agent)

(defparameter *url* "http://localhost:11434/v1/chat/completions")
(defparameter *tools* (jzon:parse "[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}]"))

(defun obj (&rest kv)
  (let ((h (make-hash-table :test #'equal)))
    (loop for (k v) on kv by #'cddr do (setf (gethash k h) v)) h))

(defun chat (messages)
  (let* ((body (jzon:stringify (obj "model" "llama3.1" "messages" (coerce messages 'vector) "tools" *tools*)))
         (res (jzon:parse (dex:post *url* :content body :headers '(("Content-Type" . "application/json"))))))
    (gethash "message" (aref (gethash "choices" res) 0))))

(defun run (prompt)
  (let ((messages (list (obj "role" "user" "content" prompt))))
    (loop repeat 5
          for msg = (chat messages)
          for calls = (gethash "tool_calls" msg)
          do (setf messages (append messages (list msg)))
             (when (or (null calls) (zerop (length calls)))
               (return (gethash "content" msg)))
             (loop for c across calls
                   for args = (jzon:parse (gethash "arguments" (gethash "function" c)))
                   do (setf messages (append messages
                        (list (obj "role" "tool" "tool_call_id" (gethash "id" c)
                                   "content" (format nil "It is 24°C and sunny in ~a." (gethash "city" args))))))))))

(print (run "Should I take an umbrella in Lahore today?"))

jzon parses JSON objects into hash tables, so fields are read with gethash.

Run it locallyInstall Ollama, run ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.
Part IV · Your path

Lisp learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

CalculatorEvaluate maths expressions written as lists.
Expert systemA rule-based advisor for choosing a GPU.
Hybrid AICombine Lisp rules with an LLM for answers.
Reference

Lisp cheat sheet

TaskSyntax
Global(defparameter *x* 5)
Function(defun add (a b) (+ a b))
Lambda(lambda (x) (* x 2))
Local(let ((x 1)) ...)
Map list(mapcar #'f list)
Print(format t "~a~%" x)
Reference

Common mistakes

  • Quoting matters: '(a b) is data, (a b) calls function a.
  • Use equal or string= to compare strings; eq compares identity.
  • Different dialects (Common Lisp, Scheme, Clojure) look similar but are not compatible.
Reference

Where to go next

On TensorCore.si: Clojure volume, Scheme volume.

Keyboard: [ previous language · ] next language · / search the library