A Common Lisp library for reading, modifying, and writing Lisp source code while preserving whitespace, comments, and formatting.
rewrite-cl parses Common Lisp source into an AST that retains all formatting information, allowing you to programmatically transform code and write it back without losing the original style. This is useful for:
- Code refactoring tools
- Automated code modifications
- Linters and formatters
- Source-to-source transformations
Clone this repository and load via ASDF:
(asdf:load-system "rewrite-cl")(asdf:load-system "rewrite-cl")
(use-package :rewrite-cl)
;; Parse a string and get back the exact same string
(node-string (parse-string "(defun foo (x) (+ x 1))"))
;; => "(defun foo (x) (+ x 1))"
;; Parse preserves whitespace and comments
(node-string (parse-string "(foo bar ; comment
baz)"))
;; => "(foo bar ; comment
;; baz)"
;; Use zippers for navigation and editing
(let ((z (of-string "(defun foo (x) x)")))
(setf z (zip-down z)) ; enter the list
(setf z (zip-right* z)) ; skip to 'foo' (skipping whitespace)
(setf z (zip-replace z (make-token-node 'bar "bar")))
(zip-root-string z))
;; => "(defun bar (x) x)"The AST is composed of nodes. Each node has:
node-tag- A keyword identifying the type (:symbol,:list,:whitespace, etc.)node-string- The exact source text representationnode-sexpr- The evaluated Lisp value (when applicable)node-children- Child nodes (for compound nodes)
Node types include: :symbol, :keyword, :integer, :float, :ratio, :string, :character, :list, :vector, :quote, :syntax-quote, :unquote, :unquote-splicing, :whitespace, :newline, :comment, :block-comment, and various reader macro types.
;; Parse a single form
(parse-string "(+ 1 2)")
;; Parse all forms (returns a list)
(parse-string-all "x y z")
;; Parse from a file
(parse-file "mycode.lisp")
(parse-file-all "mycode.lisp")Zippers provide a functional way to navigate and edit the AST:
(let ((z (of-string "(a (b c) d)")))
;; Navigation
(zip-down z) ; Move to first child
(zip-up z) ; Move to parent
(zip-left z) ; Move to left sibling
(zip-right z) ; Move to right sibling
(zip-next z) ; Depth-first traversal
;; Whitespace-skipping variants (skip whitespace/comments)
(zip-down* z)
(zip-right* z)
;; Editing (returns new zipper)
(zip-replace z new-node)
(zip-insert-left z node)
(zip-insert-right z node)
(zip-remove z)
;; Get results
(zip-root-string z) ; Get modified source string
(zip-sexpr z)) ; Get s-expression at point;; From Lisp values (automatic conversion)
(coerce-to-node 'foo)
(coerce-to-node '(a b c))
;; Explicit constructors
(make-token-node 'symbol "symbol")
(make-string-node "hello" "\"hello\"")
(make-list-node (list child1 child2))
(make-comment-node "; my comment")
(spaces 4) ; 4 spaces
(newlines 2) ; 2 newlinesparse-string- Parse first form from stringparse-string-all- Parse all forms from stringparse-file- Parse first form from fileparse-file-all- Parse all forms from file
node-tag- Get node type keywordnode-string- Get source textnode-sexpr- Get Lisp valuenode-children- Get child nodesnode-inner-p- Check if node has childrencoerce-to-node- Convert Lisp value to node
of-string- Create zipper from stringof-file- Create zipper from fileof-node- Create zipper from node
zip-up,zip-down,zip-left,zip-right- Basic movementzip-down*,zip-left*,zip-right*- Skip whitespace/commentszip-next,zip-prev- Depth-first traversalzip-next*,zip-prev*- Depth-first, skipping whitespace
zip-replace- Replace current nodezip-edit- Apply function to current nodezip-insert-left,zip-insert-right- Insert siblingszip-insert-child,zip-append-child- Insert childrenzip-remove- Remove current node
zip-node- Get current nodezip-root- Get root nodezip-root-string- Get full source stringzip-sexpr- Get s-expression at pointzip-tag- Get current node's tag
zip-find,zip-find-tag,zip-find-value- Searchzip-find-all- Find all matching nodeszip-find-in-children,zip-find-child-value- Search in childrenzip-prewalk,zip-postwalk- Tree walking with transformationzip-collect,zip-collect-sexprs- Collect nodes during traversalzip-transform,zip-transform-if- Transform matching nodeszip-map-children- Map over childrenzip-nth-child- Access nth child directly
rewrite-cl is directly inspired by and draws heavily from rewrite-clj, the excellent Clojure library for rewriting Clojure code. The zipper-based approach, API design, and overall architecture owe much to that project.
MIT License
Anthony Green green@moxielogic.com