
\documentclass[12pt]{article}
\usepackage[T2A,OT1]{fontenc}
\usepackage[default]{cantarell}
\usepackage[a4paper, top=20mm, bottom=20mm, left=20mm, right=20mm]{geometry}
\usepackage[utf8]{inputenc}
\usepackage[russian, english]{babel}
\usepackage{tabu}
\usepackage{hyperref}
\usepackage{parskip}
\usepackage{graphicx}
\usepackage{tabularx}
\usepackage[normalem]{ulem}
\usepackage{float}
\floatstyle{boxed}
\restylefloat{figure}
\usepackage{setspace}
\onehalfspacing
\author{Artyom Bologov \href{mailto:lisp-lines@aartaka.me}{(email)}}
\date{\today}
\title{Line-based Lisp Editing}
\makeatletter
\def\endenv{\expandafter\end\expandafter{\@currenvir}}
\makeatother
\begin{document}
\maketitle

\includegraphics[width=\textwidth,height=\textheight,keepaspectratio]{./assets/lisp-lines.png}

So I have these two wolves in me: Lisp and ed(1).
One problem with uniting these weird parts of me, though: Lisp is not editable in ed!
At all.
ed is a line-oriented editor, while Lisp is tree-based, with most forms and “statements” spanning multiple lines.
ed was made to program Assembly, C, or configs; not Lisp.

So are there any ways to converge on these two: somehow making Lisp line-editable, and somehow making ed(1) Lisp-aware?
That’s what I’m tumbling over in this post:

\section*{Typical Lisp} \label{lisp}

Lisp (as written by a professional) is usually condensed into the most effective AST form.
No stray brackets on a line of their own.
No unnecessary line breaks after control structures.
And no way to know where one form ends and another starts.
(Unless you use a Lisp-aware editor
\href{run:lisp-indent}{and/or semantic indentation}.)

\begin{figure}[h!]\begin{verbatim}
(defun factorial (n)
  (if (< n 2)
      1
      (* n (factorial (1- n)))))
\end{verbatim}\caption{(Unoptimized) factorial in Common Lisp}\end{figure}

And even though I have these macros in my \verb|.inputrc|
\href{https://github.com/aartaka/emvi}{and a whole vi(1) setup for Lisp editing},
I’m still frightened by the prospect of editing my Lisp code in ed...

\begin{figure}[h!]\begin{verbatim}
"\C-xu": "s/^\\([[:space:]]*\\)(\\(\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*\\))/\\1\\2/n"
"\C-xw": "s/^\\([[:space:]]*\\)\\(\\(\\((\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^() ]\\{0,1\\}\\)*\\)/\\1(\\2)/n"
"\C-xs": "s/(\\(\\(\\((\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^() ]\\{0,1\\}\\)*\\)) \\(\\(\\((\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^() ]\\{0,1\\}\\)*\\)/(\\1 \\9)/n"
\end{verbatim}\caption{inputrc bindings for (respectively) unwrap, wrap, and slurp (yes, I drew them myself, by hand)}\end{figure}

No, even these can’t handle nested parentheses and multiple forms on the same line.

\section*{Line-by-Line Lisp (NDLISP)} \label{line-lisp}

So what if we somehow made Lisp forms more line-oriented?
\href{https://stackoverflow.com/questions/47602677/recursive-factorial-function-in-common-lisp}{This is what many university courses do to students}:

\begin{figure}[h!]\begin{verbatim}
(defun factorial(n)
    (if (= n 1)
            (setq a 1)
    )
    (if (> n 1)
        (setq a (* n (factorial (- n 1))))
    )
    (format t "~D! is ~D" n a)
)
\end{verbatim}\caption{Ugly parentheses and line breaks Lisp (don’t mind the thing, it’s an amateur experiment from StackOverflow)}\end{figure}

Repuling.
Disgusting.
Wile.

But there’s an insight in that!
What if we put significant self-contained forms on their own lines and edited them one by one?
\href{https://jsonlines.org}{This is the idea behind JSON Lines}
\href{https://github.com/ndjson/ndjson-spec}{(and NDJSON—Newline-Delimited JSON.)}
So why not have our own NDLISP?

\subsection*{Non-Portable Lines: Threading Macros and LOOP} \label{macros}

Clojure changed the world (did it?) with the notion of
\href{run:threading}{threading macros}.
Now one can finally write Lisp with straight top-down causal relations!
And subvert deep indentation and all the structure complications coming from that.
So threading macros are a good basis for line-based Lisp editing, actually.

\begin{figure}[h!]\begin{verbatim}
(defn fac [n]
  (if (< n 2)
    1
    (-> n
        dec
        factorial
        (* n))))
\end{verbatim}\caption{Clojure factorial with threading macros (yeah, I know, quite contrived)}\end{figure}

Except…
CL and Scheme are too chaotic compared to Clojure.
Brown field vs. green field, y’know.
\href{https://srfi.schemers.org/srfi-197/}{Scheme threading / chain / pipeline SRFI requires explicit threading argument}, for example.
Common Lisp arrow macros libraries are numerous yet hard to use.
CL simply has too many arguments to functions.
Including keyword arguments.

But CL has almost infinite potential with its macros.
Including the standard LOOP macro.
It’s the one with syntax resembling mainstream non-Lisp languages:

\begin{figure}[h!]\begin{verbatim}
(defun factorial (n)
  (loop with res = 1
        for i from n downto 1
        do (setf res (* res i))
        finally (return res)))
\end{verbatim}\caption{CL LOOP macro, almost like BASIC}\end{figure}

LOOP (sorry for all-caps, it’s a convention) can do a lot of things.
\href{https://github.com/aartaka/advent-of-code-in-cl-loop}{I did most of Advent of Code exclusively with LOOPs once}!
\href{run:lisp-indent}{Just don’t go too wild with indentation}…
Oh, wait.

\section*{Indentation-based Syntaxes: T-exprs, Wisp, etc.} \label{indent}

Line-isolated forms only get one so far.
There’s unease and the idea that Lispers actually don’t care about parentheses at all.
Lispers care about indentation derived from parens.
Thus the plenty indentation-based syntaxes deprecating parentheses and lines:

\begin{itemize}\item \href{https://srfi.schemers.org/srfi-110/srfi-110.html}{Sweet-expression},
\item \href{https://srfi.schemers.org/srfi-119/srfi-119.html}{Wisp},
\item \href{https://dpk.land/tmp/bonsai.html}{And a recent proposal by Daphne Preston-Kendal} (that I didn’t read, I admit.)
\end{itemize}

These remove one hard part of editing Lisp: parentheses and blocks.
But such syntaxes also introduce a significant problem: indentation.
Indentation is invisible and indiscernible.
Is it five spaces or four?
A tab, maybe?
What width is a tab even?
Does anyone use tabs in the modern world?
(They do, they do, they do...)

Spaces are hard, in ed too.
Most lines have both a leading and contained spaces.
Batch operations on lines are risky.
Learning which type of spaces is used in the codebase requires constant \verb|l| (unambiguous printing.)
And I prefer \verb|n| (printing with line numbers!)

\paragraph{Going against myself here} \begin{quote}
The fact that
\href{scripts/wisp.ed}{I wrote a Wisp-aware ed script}
defeats my point, but let’s ignore it and see where the post gets us.
\end{quote}

\section*{Whitespace-Insensitive: Lua} \label{lua}

So what if we actually forgot about both parentheses and indentation?
(To an extent, I mean!)
We get Lua:

\begin{figure}[h!]\begin{verbatim}
function fact(n)
  if n == 0
    then return 1 end
  if n < 0
    then return 0 end
  return n * fact(n-1)
end
\end{verbatim}\caption{Totally unambiguous factorial function in Lua}\end{figure}

True, some forms (\verb|function|, \verb|if|, \verb|do|) need an \verb|end|.
But most regular forms, like assignments, function calls, and return statements—don’t!
So making a line-based Lisp means designing an unambiguous set of operations.
Mapped from special forms.
And extensible with macros.
(That’s the hardest part, not sure if it’s even doable.)
\href{https://wikipedia.org/wiki/Dylan_(programming_language)}{Like Dylan, but stripped down}.

\section*{Whitespace-Insensitive + Minor Delimiters: Lamber!} \label{lamber}

\href{https://github.com/aartaka/lamber}{This unambiguous basics type of thing is exactly what Lamber does}.
Establishing an unambiguous set of fundamental forms and a set of shortcuts.
And being a reasonable non-homoiconic (means no macros yet) syntactic Lisp otherwise:

\begin{figure}[h!]\begin{verbatim}
def factorial function (n)
  if (= n 0)
   then 1
  else
   * n : factorial : dec n .
\end{verbatim}\caption{Lamber factorial, perfectly line-editable}\end{figure}

At this point, as an ed fan and a biased programming language author—I’m satisfied with the syntax.
One problem though...
Extrapolating this toy language syntax to Common Lisp

\section*{Middle Ground: Reader Macro} \label{reader}

Even if there’s a way to bring Lamber’s a-ha! moments to Lisp...
\href{https://github.com/aartaka/lamber/blob/master/source/reader.lisp}{It requires a full-blown Lamber parser}.
And a set of complex transformations from Lamber to Lisp.
Not really the best time I can imagine.

So why not use Lisp-native reader macros, settle for some less, and retain the best parts?
Say, Wisp’s colon operator coupled with general whitespace-insensitivity of Lamber/Lisp.
\href{https://codeberg.org/aartaka/lisp-config/src/commit/d5c3a290354507262e946c3c123dc284ebce3a22/reader.lisp#L192}{Behold, hash/slash operator}!

\begin{figure}[h!]\begin{verbatim}
(defun factorial (n)
  (if #/ < n 2
      1
      #/ * n #/ factorial #/ 1- n))

;; Same as
(defun factorial (n)
  (if (< n 2)
      1
      (* n (factorial (1- n)))))
\end{verbatim}\caption{hash/slash operator, introducing a new function/macro/special form}\end{figure}

While it doesn’t remove all the parentheses (it doesn’t have to!)
And doesn’t make the code readable to a standard Lisp user.
It makes things much easier to edit in a line-based environment.
I’ve reached what I need.
Did you?

I accumulated enough of such line-based editing hacks and syntaxes to abstract into a library.
\href{https://codeberg.org/aartaka/lin-edi-table}{Find it on Codeberg, lin-edi-table}!


\par\noindent\rule{\textwidth}{0.4pt}
\href{https://creativecommons.org/licenses/by/4.0}{CC-BY 4.0} 2022-2026 by Artyom Bologov (aartaka,)
\href{https://codeberg.org/aartaka/pages/commit/a91befa}{with one commit remixing Claude-generated code}.
Any and all opinions listed here are my own and not representative of my employers; future, past and present.
\end{document}
