screwlisp proposes kittens

:screwlisps-knowledge/tangle common lisp asdf package markdown tangling and lisp pathnames

Here I exhibit a simple use of cl-series’ scan-file and collect-file to tangle a markdown file into a lisp package in a specified lisp system. Further, I add a system stubbing function which uses lisp pathnames, which are a tricky topic.

My motivation was to at least in common lisp reconcile my small-web markdown and lisp systems, by defining my lisp systems as tangles of my small-web markdown. Edrx showed me that something like this could be done by modifying emacs eev’s code-c-d, but, well, I know one hundred percent how what I just wrote here works.

It is a little hard to remember that common-lisp pathname stuff, as Kent fairly frequently says: it was written for back when there was more than one style of filesystem. However, after thinking a bit about it, I am fairly happy with it. the gist is that I have

  1. Always used make-pathname.
  2. Represented unknown paths as relative and wild ((:relative :wild)).
  3. Made a new pathname when you know more of the path.

Wild pathnames can have more advanced uses, but this is not a bad idiom on modern systems anyway I think. The modern string convention is I think, more primitive.

eev setup

This is a block comment, prior to the required package specification. Elsewhere I have talked about my focus on embeddable common lisp.

#|
;; Allowing, the quicklisp links are for sbcl and sly. Still, it gives an idea.
 (find-quicklisp-links "series")
 (setq inferior-lisp-program "ecl")
 (slime)
 (setq eepitch-buffer-name "*slime-repl ECL*")

(require :series)
|#

:screwlisps-knowledge/tangle package definition

This must be the first lisp expression in the file (since from our :package-inferred-system useage, experientially files won’t exist but packages do).

(uiop:define-package :screwlisps-knowledge/tangle
    (:import-from :series install)
  (:mix :asdf :uiop :cl)
  (:export :tangle)
  (:nicknames :sk/tangle :screwniverse/tangle))
(in-package :sk/tangle)
(eval-when
    (:compile-toplevel :execute)
  (install))

For those who aren’t used to common-lisp, asdf, and uiop:

ASDF :package-inferred-system details

This is in my view a modern residential source filesystem in the sense that there is no “file” - only packages, where packages are inside systems. In order to make this work, each source file created in a system starts with either defpackage or uiop:define-package, and the package generally needs to be named :system/dir1/dir2/dir3/filename (downcased).

Then, in the absense of a (:mix ..), (:mix :cl) is assumed if I recall correctly, however I specifically import install from series.

In the new package=file, at compile-toplevel, I call cl-series’ install (which temporarily interns series’ exported symbols into the current or specified package).

Interactively muddling through it once

Setup a test input and output file

Let’s do this in a comment in the file. In some sense, it is documentation of what we did. Let’s also use the common lisp formatted output ~@\newline directive which will let us use indented '''s safely.

(with-open-file
    (*standard-output* #p"/tmp/test-tangle.md"
		       :direction :output
		       :if-does-not-exist :create)
  (format *standard-output*
	  "# An MD file~@
           having~@
           ```~@
           (lisp code)~@
           ```~@
           blocks~@
           ## Such as~@
           ```~@
           ;; this.~@
           ```~@
            ``` but not this.~@
           ."))
             

Let’s read the codes of that using cl-series

cl-Series provides working fakes of its static-analysis-generated tight, lazy, efficient functionality. However, the fakes are not pure common lisp. But you can use them to explore interactively in series’ declarative style, and see what-you’re-working-with while you’re working on it, as Hairylarry for example mentioned doing.

(defparameter *filein* (scan-file #p"/tmp/test-tangle.md"
				  #'read-line))
*filein*

Using eev-mode, on my right side I see:

#|
SK/TANGLE> (defparameter *filein* (scan-file #p"/tmp/test-tangle.md"
					     				  #'read-line))
*FILEIN*
SK/TANGLE> *filein*
#Z("# An MD file " "having " "```" "(lisp code)" "```" "blocks" "## Such as" "```" ";; this." "```" ".")
SK/TANGLE>
|#

I guess, I would like to know where lines starting with ''' are.

(defparameter *codeblock-toggles*
  (#Mstring= (series "```") *filein*))
*codeblock-toggles*

Series’ series creates an infinite series of whatever its arguement is, which I compare to the finite series of lines read-lineed from the file. We don’t have to personally manage the end of file stuff.

Outputs:

#|
SK/TANGLE> (defparameter *codeblock-toggles*
	       (#Mstring= (series "```") *filein*))
*CODEBLOCK-TOGGLES*
SK/TANGLE> *codeblock-toggles*
#Z(NIL NIL T NIL T NIL NIL T NIL T NIL)
SK/TANGLE> 
|#

Looking good. Now here we come to an annoying-ish point for my personal intuitions about series. Sometimes we need to use a toggle as I am about to, because we-don’t-know-what-happens-in-the-future. This is quite different to eagerly evaluated code we may be used to.

(defparameter *blocksp*
  (let* ((in-block nil))
    (mapping
     ((maybe-latch *codeblock-toggles*)
      (line *filein*))
     (if in-block
	 (if maybe-latch
	     (setq in-block (not in-block))
	     line)
	 (and maybe-latch
	      (setq in-block (not in-block))
	      nil)))))

Well, let’s do an assertion.

(assert (equal "(lisp code)"
	       (collect-nth 3 *blocksp*))
	nil
	"~a should be "(lisp code)" in the test example."
	(collect-nth 3 *blocks*))

I guess we just want the lines, and write-lineed to a file.

(collect-file
 "/tmp/test-tangle.lisp"
 (choose *blocksp* *blocksp*)
 'write-line)

The tangle function

Let’s just trivially collect our test assert case into a function.

(defun tangle
    (md-path system
     &aux
       (fileout (make-pathname
		 :name (pathname-name md-path)
		 :type "lisp"
		 :directory
		 (pathname-directory
		  (system-source-directory system)))))
  (let* ((filein (scan-file md-path #'read-line))
	 (codeblock-toggles (#Mstring= (series "```")
				       filein))
	 (blocksp
	   (let ((in-block nil))
	     (mapping
	      ((maybe-latch codeblock-toggles)
	       (line filein))
	      (if in-block
		  (if maybe-latch
		      (setq in-block (not in-block))
		      line)
		  (and maybe-latch
		       (setq in-block (not in-block))
		       nil))))))
    (collect-file
     fileout
     (choose blocksp blocksp)
     'write-line)))

System directory stubbing

I guess one should probably not want this. Still, here it is.

(defun stub-system
    (namestring)
  (let* ((homedir (user-homedir-pathname))
	 (dir (make-pathname :directory
			     `(:relative ,namestring)))
	 (lispdir (make-pathname :directory
				 '(:relative "common-lisp")))
	 (sysdir
	   (reduce 'merge-pathnames
		   (list dir lispdir homedir)))
	 (asd
	   (make-pathname :name namestring
			  :type "asd"
			  :directory '(:relative :wild)))
	 (system-asd
	   (make-pathname
	    :directory (pathname-directory sysdir)
	    :name (pathname-name asd)
	    :type (pathname-type asd))))
    (ensure-directories-exist sysdir)
    (with-open-file
	(*standard-output* system-asd
			   :direction :output
			   :if-does-not-exist :create
			   :if-exists :error)
      (format t
	      "(defsystem ~a ~
	        :class :package-inferred-system)"
	      (intern namestring :keyword))
      (probe-file
       (make-pathname
	:directory (pathname-directory sysdir)
	:name (pathname-name asd)
	:type (pathname-type asd))))))

Example

(eval-when
    ()
  (stub-system "foo")
  (tangle #p"/tmp/test-tangle.md"
	  "foo")
  )

it will error if #p"~/common-lisp/foo/foo.asd" already exists. It works đŸ˜ș

Conclusion

While my intended focus was the first two, I found I usefully covered three topics here:

  1. Tangling markdown into an asdf :class :package-inferred-system lisp system
  2. Doing so with scan-file and collect-file from series
  3. Working with lisp’s make-pathname directories.

I do think that make-pathname is more sophisticated than primitive string-processing style modern convention pathnames. Hope it helps someone get used to these three topics.

Currently works on my machine!

Fin.

I guess this article is somehow both very simple and very intricate. I would like to hear your improvements, alternatives, or memories of what-you-would-do on the mastodon thread.

Because our article shows all of

as always, I would appreciate you sharing it in any particular means and manner that occurs to you personally.

Small note: When I used the system on itself, my markdown file was named #p"tangle.page.md" so the lisp file it generated from itself used metacircularly was #p"~/common-lisp/screwlisps-knowledge/tangle.page.lisp" - I didn’t think of how I want to deal with other programms “subtyping” file types like this, so I just fixed it directly for now. If you don’t subtype files inside the filename, I guess it just works.

screwlisp proposes kittens