Automatic Knowledge Graph of Common Lisp code via live image introspection.
Given a package loaded in the SBCL image, builds and maintains an Ariadne graph of its symbols, class hierarchies, method specializations, call relationships, and metadata — all without parsing source code. Includes a live Emacs integration that shows code intelligence as you navigate.
Clone into your Quicklisp local-projects:
cd ~/quicklisp/local-projects/
git clone https://git.sr.ht/~hajovonta/cl-codegraph
git clone https://git.sr.ht/~hajovonta/ariadneAdd to your init file:
(add-to-list 'load-path "~/quicklisp/local-projects/cl-codegraph/emacs/")
(require 'cl-codegraph)
(cl-codegraph-enable-globally) ;; activates in all lisp-mode buffersOr, to enable per-buffer instead: M-x codegraph-mode in any Lisp buffer.
Requires glue (available on MELPA).
Start your Lisp image and load your project as usual, then:
(ql:quickload :cl-codegraph)That's it. No further setup needed — everything else is automatic.
- Open any CL source file in Emacs
- Move your cursor to a symbol — the
*codegraph*buffer appears showing type, args, docstring, callers, callees - Click (RET) on any symbol in
*codegraph*to jump to its source and update the view - Press
?for aggregate queries (dead exports, cycles, impact analysis, call chains) - Press
vto visualize in the Graph Explorer web UI (auto-starts at http://localhost:8080/)
Package detection, graph building, monitoring, and incremental updates all happen automatically on first access.
;; Adjust idle delay before updating (default: 0.3s)
(setq cl-codegraph-idle-delay 0.5)
;; Change Graph Explorer port (default: 8080)
(setq cl-codegraph-explorer-port 9090)If you prefer to start the web server manually (e.g., different port, or before first v):
(ariadne:start-web-server :port 9090)The *codegraph* buffer is a live code intelligence panel:
- Automatic — detects package from
(in-package ...), auto-monitors on first access - Live — updates as you move cursor (debounced, async, non-blocking)
- Navigable — RET on a caller/callee jumps to its source AND updates the view
- History —
lgoes back (browser-style),qcloses - Per-method — GFs show each method's specializers and individual callees
- Classes — shows superclasses, subclasses, slots, specializing methods
- Local variables — falls back to source walking for let/dolist/dotimes bindings
- Transient menu —
?for aggregate queries (dead exports, cycles, impact, call-chain, etc.) - Smart completion — bare names when unique, qualified when ambiguous
| Key | Action |
|---|---|
| RET | Visit symbol: jump to source + update view |
| l | Go back in history |
| v | Visualize in Graph Explorer (auto-starts web server) |
| ? | Transient menu (aggregate queries) |
| q | Close window |
Press v in the *codegraph* buffer to visualize the current symbol or query result in Ariadne's web-based Graph Explorer. On first use, the web server starts automatically at http://localhost:8080/.
- Symbol view: focuses the explorer on the node with depth-1 neighborhood
- Impact result: sends a SPARQL query showing all impacted call edges
- Call chain: sends the chain as a subgraph query
Monitored graphs are automatically registered in the Explorer's graph selector dropdown.
;; Change the explorer port (default: 8080)
(setq cl-codegraph-explorer-port 9090);; Build a graph manually
(defparameter *g* (cl-codegraph:build-graph :my-package :include-internal t))
;; Query helpers
(cl-codegraph:what-calls *g* "pkg:fn") ;; what does it call?
(cl-codegraph:who-calls-p *g* "pkg:fn") ;; who calls it?
(cl-codegraph:call-chain *g* "pkg:a" "pkg:b") ;; path from A to B
(cl-codegraph:dead-exports *g*) ;; exported but never called
(cl-codegraph:undocumented-exports *g*) ;; missing docstrings
(cl-codegraph:find-cycles *g*) ;; circular dependencies
(cl-codegraph:impact-of *g* "pkg:fn") ;; what breaks if I change this?
(cl-codegraph:fan-in *g* "pkg:fn") ;; how many call it
(cl-codegraph:fan-out *g* "pkg:fn") ;; how many it calls
(cl-codegraph:unused-packages *g* :my-package) ;; unused use-list entries
;; Visualization
(cl-codegraph:export-dot *g* :predicates '("cg:calls"))
(cl-codegraph:neighborhood *g* "pkg:fn" :depth 2)
;; Change detection
(cl-codegraph:diff-summary *before* *after*)
(cl-codegraph:refresh-graph *g* :my-package :include-internal t)
;; Live monitoring (used automatically by Emacs integration)
(cl-codegraph:monitor :my-package :include-internal t)
(cl-codegraph:graph :my-package) ;; always current — flushes dirty symbols
(cl-codegraph:unmonitor :my-package)
;; Large packages (>200 symbols) are indexed in a background thread.
;; monitor returns immediately; aggregate queries work once indexing completes.
(setf cl-codegraph:*background-index-threshold* 500) ;; tune if needed
;; Multi-package
(cl-codegraph:build-multi-graph '(:pkg-a :pkg-b))
(cl-codegraph:build-system-graph :my-system)
;; REPL
(format t "~A" (cl-codegraph:describe-symbol *g* "pkg:fn"))
(format t "~A" (cl-codegraph:summary *g*))| Predicate | Meaning |
|---|---|
rdf:type |
Symbol kind: function, generic-function, class, macro, constant, special-variable |
cg:inPackage |
Symbol → package |
cg:exports |
Package → symbol |
cg:internal |
"true" for non-exported symbols |
cg:calls |
Function/method → function it calls, OR variable/constant → function its init-form calls |
cg:calledBy |
Function → function that calls it, OR function → variable/constant whose init-form calls it |
cg:subclassOf |
Class → superclass |
cg:hasSlot |
Class → slot |
cg:methodOf |
Method → generic function |
cg:specializesOn |
Method → class it specializes on |
cg:dependsOn |
Package → external package |
cg:lambdaList |
Function → parameter list string |
cg:docstring |
Symbol → documentation string |
cg:sourceFile |
Symbol → source file path |
cg:value |
Constant/variable → current value |
cg:expandsMacro |
Function → macro it expands |
cg:readsVar |
Function → variable/constant it reads |
cg:writesVar |
Function → variable it writes |
cg:external |
"true" for cross-package callees |
No source parsing for the vast majority of the graph. All data comes from the live SBCL image:
- Symbol enumeration:
do-external-symbols/do-symbols - Call graph:
sb-introspect:find-function-callees(forward, O(n)) - GF method bodies:
sb-pcl::safe-method-fast-functionpierces the PCL trampoline - Signatures:
sb-introspect:function-lambda-list - Source locations:
sb-introspect:find-definition-sources-by-name - Macro usage:
sb-introspect:who-macroexpands - Variable deps:
sb-introspect:who-references/who-sets - Class structure: MOP (
class-direct-superclasses,class-direct-slots) - Live updates:
sb-int:*setf-fdefinition-hook*+ encapsulated%defvar/%defparameter/load-defclass
One exception: a defparameter/defvar/defconstant init-form is evaluated once and discarded — the symbol ends up boundp but never fboundp, so find-function-callees has no compiled function object to introspect and can't see calls made from the init-form. For this one case, cl-codegraph reads the actual init-form (via sb-introspect:find-definition-sources-by-name to locate the file, then a package-aware source read) and walks the expression for calls to other symbols in the same package, emitting the same cg:calls/cg:calledBy edges. This only works when file-backed source is available (symbols defined at the REPL are skipped silently) and is currently scoped to package-local calls only.