100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Code as Data: Homoiconicity

Why LISP source code and LISP data share the same list representation, and how this property enables macros, metaprogramming, and eval.

Macros & MetaprogrammingAdvanced9 min readJul 10, 2026
Analogies

What Homoiconicity Means

Homoiconicity means a language's source code is represented using the same data structures the language itself manipulates. In LISP, both code and data are built from S-expressions, parenthesized lists of symbols, numbers, and nested lists, so the expression (+ 1 2) is simultaneously 'a function call adding 1 and 2' when evaluated, and 'a three-element list' when treated as data. This dual nature is what lets LISP programs read, construct, and rewrite other LISP programs using the same list operations they'd use on any other data.

🏏

Cricket analogy: Homoiconicity is like a scorecard that is simultaneously the official record and the input to the next over's tactics — the same sheet of numbers a captain reads is also what the analyst feeds back into planning, no separate format needed.

S-Expressions as the Common Format

Because S-expressions are just nested lists, a LISP program can traverse another program's structure using ordinary list operations like car (first element) and cdr (rest of the list). Given the form (+ 1 2), (car '(+ 1 2)) returns the symbol + and (cdr '(+ 1 2)) returns the list (1 2), the same two primitives you'd use to walk any list of data. There is no separate 'abstract syntax tree' API to learn; the AST is the list structure you already know how to manipulate.

🏏

Cricket analogy: Walking code with car/cdr is like reading a scorecard's over-by-over list using the same 'first ball, remaining balls' logic you'd apply to any sequence, without needing a special scorecard-parsing tool.

lisp
;; Code is just nested lists  you can inspect and build it with list operations
(car '(+ 1 2))        ; => +
(cdr '(+ 1 2))        ; => (1 2)

;; Construct new code as data, then evaluate it
(defparameter *expr* (list '+ 1 2))
(eval *expr*)          ; => 3

;; A macro-like helper built entirely from list manipulation
(defun negate-form (form)
  (list 'not form))

(eval (negate-form '(> 2 3)))   ; => T

Why This Enables Macros and Code Generation

Because programs are ordinary data, LISP programs can generate other programs at runtime or compile time, the foundation of macros, embedded domain-specific languages, and metaprogramming in general. A LISP program can build up an S-expression piece by piece with list, cons, and quasiquote, then hand it to eval or splice it into a macro expansion, blurring the line between 'writing code' and 'writing a program that writes code.' Languages without homoiconicity, like Java or Python, must instead expose a separate reflection or AST API (e.g. Python's ast module) to achieve anything comparable, and that API's shape rarely matches the language's own syntax.

🏏

Cricket analogy: This is like a team's analytics department not just reading match data but algorithmically generating new tactical playbooks from it — the playbook and the data share the same format, so one can produce the other directly.

Contrast this with a typical non-homoiconic language: to programmatically inspect or generate code in Python you reach for the separate ast module, and the resulting tree's shape looks nothing like ordinary Python data. In LISP there is no such gap — the list you'd build with cons and the code the reader parses are the same kind of object.

eval and Its Risks

eval takes a LISP data structure and evaluates it as code, the natural counterpart to homoiconicity, since code is data, you can always ask the interpreter to run some data as a program. This is genuinely useful for building interpreters, configuration systems, or REPL-driven tools, but it is also a security liability: calling eval on data that originated from an untrusted source (user input, a network request, a file you don't control) means that source can run arbitrary code with your program's full privileges. Production systems generally restrict eval to trusted, internally generated forms, or avoid it entirely in favor of macros resolved at compile time.

🏏

Cricket analogy: Using eval on untrusted data is like letting a random spectator's suggested tactic get automatically executed by the captain mid-over — trusting an unverified source with real, live authority over the match.

eval is a security-sensitive operation: never call eval on data that originates from an untrusted source (user input, network payloads, uploaded files). If you need to execute dynamic behavior safely, prefer a restricted interpreter, sandboxing, or macros expanded at compile time over raw eval on arbitrary input.

  • Homoiconicity means LISP code and LISP data share the same representation: nested lists of symbols (S-expressions).
  • car and cdr, the same primitives used on any list, are sufficient to inspect and traverse code structure — no separate AST API is needed.
  • Programs can construct new code as data (with list, cons, quasiquote) and then run it with eval, or splice it into a macro expansion.
  • This is the foundation of macros and metaprogramming in LISP, letting programs write programs.
  • Non-homoiconic languages need a separate reflection/AST API (e.g. Python's ast module) whose shape doesn't match the language's own syntax.
  • eval on untrusted input is a serious security risk and should be avoided or heavily sandboxed in production systems.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#LISPStudyNotes#CodeAsDataHomoiconicity#Code#Data#Homoiconicity#Means#StudyNotes#SkillVeris#ExamPrep