git » blacktide.git » commit 649278b

Refine tutorial opening and correct the inversion example

author Alan Dipert
2026-11-08 06:43:20 UTC
committer Alan Dipert
2026-11-08 06:43:20 UTC
parent 0cc633c4364879cb16bdb284262c39e65d1f3b01

Refine tutorial opening and correct the inversion example

doc/tutorial.md +24 -47

diff --git a/doc/tutorial.md b/doc/tutorial.md
index 41988aa..ae4bd54 100644
--- a/doc/tutorial.md
+++ b/doc/tutorial.md
@@ -2,60 +2,43 @@
 
 Hey, so, I've been thinking about mutable local variables in Clojure. Is that OK? Hear me out.
 
-The Clojure situation has been explained to me. I'm down with the philosophy. Immutable values. Persistent data structures. A more civilized, decomplected age. But sometimes I have these intrusive thoughts about mutating local variables. I'll be reading an imperative algorithm in pseudocode, Java or C, and—please don't tell anyone about this—I'll think: man, it would be pretty sweet to port this directly without translating it into atoms or `loop`/`recur` or monads or whatever.
+The Clojure situation has been explained to me. I'm totally down with the philosophy. Stable identities, immutable values, first-class state management. A more civilized, decomplected age. But I gotta level with you. Sometimes I have these intrusive thoughts about mutating local variables. I'll be reading an imperative algorithm in pseudocode, Java or C, and—please don't tell anyone about this—I'll think: man, it would be pretty sweet to port this directly without translating it into atoms or `loop`/`recur` and whatnot.
 
-The JVM has no inherent moral objection to overwriting a [local-variable slot](https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-2.html#jvms-2.6.1). I mean, jeez, it does this for a living. Clojure's own compiler even reuses those slots to _implement_ `loop` and `recur`. But you better not dare assign to an ordinary lexical local yourself! Compiler writers only! As a mere user, you may submit a complete set of replacement values to an approved recursion point. You're welcome.
+I mean, under the covers, the JVM certainly has no inherent moral objection to overwriting a [local-variable slot](https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-2.html#jvms-2.6.1). Clojure's own compiler even reuses those slots to _implement_ `loop` and `recur`. But I'm not allowed to assign to an ordinary lexical local myself? That heady stuff is only for the elite class of compiler writers, I guess? Is it ungrateful of me, as a mere user, to not be entirely satisfied with the privilege of supplying replacement values to approved recursion points?
 
-Then there's functional programming's dirtiest implementation secret: wanton manipulation of that giant mutable variable otherwise known as "the stack." On the JVM it's [one per thread](https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-2.html#jvms-2.5.2), with mutable frames pushed and popped as methods run. We have outsourced the sin. But I digress.
-
-**Mutable local variables to the people!**
-
-`loop` and `recur` work well for most iterative algorithms. The inconvenience is that `recur` can only appear in _tail position_, where there is no further work to do in that iteration. Every loop binding needs a replacement argument, even when its value stays the same.
-
-<details>
-<summary>Try the examples in a REPL</summary>
-
-From a checkout:
-
-```sh
-clojure -T:build compile-java
-clojure -M -r
-```
-
-Load the public macros, then evaluate the Clojure blocks below in order:
+Here's an example of the kind of stuff I'd like to write on occasion. This function counts inversions in a vector: pairs where an earlier element is greater than a later one.
 
 ```clojure
 (require '[com.tailrecursion.blacktide :refer [loophole setq psetq label goto]])
-```
 
-</details>
-
-I would like to write stuff like this:
+(defn inversions [xs]
+  (loophole [n 0]
+    (dotimes [i (count xs)]
+      (dotimes [j i]
+        (when (> (nth xs j) (nth xs i))
+          (setq n (inc n)))))
+    n))
 
-```clojure
-(loophole [x 0]
-  (dotimes [i 10]
-    (setq x (+ x i)))
-  x)
-;; => 45
+(inversions [3 1 2 1])
+;; => 4
 ```
 
-Huh. So Clojure already performs the assignments needed for `loop` and `recur`. Could we generalize that trick? Write assignments where we want them, then compile them into ordinary Clojure that does the updating for us? Automate the mental gymnastics I usually perform when translating an imperative algorithm? Then we could mutate our _own_ variables!
+Nothing crazy, just setting a happy little variable `n` inside nested loops. I'm still here, by the way. Nothing terrible happened.
 
-The mechanism is already there: [`recur`](https://clojure.org/reference/special_forms#recur) evaluates its arguments, rebinds the loop locals, and starts the loop body again. In [Clojure's compiler](https://github.com/clojure/clojure/blob/clojure-1.12.6/src/jvm/clojure/lang/Compiler.java#L7156-L7227), that becomes argument evaluation followed by local-variable stores and a jump. A jump. A freaking `GOTO` of yore! We'll be wanting that too.
+But if you find yourself warming to these shenanigans, then I have some good news. This is a real thing that can be done, all thanks to the `loop`/`recur` constructs I just finished griping about. You see, [`recur`](https://clojure.org/reference/special_forms#recur) evaluates its arguments, rebinds the loop locals, and starts the loop body again. In [Clojure's compiler](https://github.com/clojure/clojure/blob/clojure-1.12.6/src/jvm/clojure/lang/Compiler.java#L7156-L7227), that becomes argument evaluation followed by local-variable stores and a jump. A jump. A freaking `GOTO` of yore! We'll be wanting that too, more in a sec.
 
-If a macro can turn our assignments and jumps into the right combination of lexical bindings, loops and `recur` forms, Clojure can do the rest. No atoms, arrays or other mutable reference cells to hold our locals, and no changes to Clojure's compiler.
+Anyway, those local-variable stores under the hood are the keys to the kingdom. We can write assignments where we want them, then compile them into ordinary Clojure that does the updating for us, via `recur`. We can automate the mental gymnastics we usually perform when translating an imperative algorithm.
 
-Doing this for a useful subset of Clojure turns out to involve writing a small compiler. Apparently I'm willing to do that.
+A macro can turn our assignments and jumps into the right combination of lexical bindings, loops, and `recur` forms, and Clojure can do the rest. No atoms, arrays or other mutable reference cells to hold our locals, and no changes to Clojure's compiler.
 
-The result is Blacktide's `loophole` macro. This walkthrough uses the compiler that's in the repository, including its intermediate data and generated code.
+Doing this for a useful subset of Clojure turns out to involve writing a small compiler. Apparently I'm willing to do that. The result is Blacktide and its `loophole` macro. **Mutable local variables (and goto!) to the people!** Allow me to give you the tour.
 
 ## The language
 
-The macro is called `loophole`. It introduces mutable lexical locals, with two assignment forms:
+The `loophole` macro introduces mutable lexical locals, with two assignment forms:
 
-- `setq` evaluates and assigns pairs sequentially, returning the last value.
-- `psetq` evaluates all right-hand sides before writing the targets, in pair order, and returns nil.
+- `setq` evaluates and assigns pairs sequentially, returning the last value. It's named after Common Lisp's `setq`, and distinct from `set!` to avoid confusion.
+- `psetq` evaluates all right-hand sides before writing the targets, in pair order, and returns nil. The `p` stands for parallel, as in "parallel assignment." Also lifted from Common Lisp.
 
 Both work in expressions. For example:
 
@@ -86,10 +69,6 @@ Falling through the body evaluates its final expression. To leave early, write a
 ;; => 45
 ```
 
-Labels belong to the outer sequence of expressions in `loophole`; wrapping them in `do` doesn't change that. A jump can leave a nested `let` or loop body, but cannot enter a lexical scope such as the body of a `let`. Jumps are forbidden inside expressions supplying values to other operations—_operands_—or computing initial values for bindings—_initializers_.
-
-The interesting part is that the body can contain ordinary Clojure constructs, including macros like `dotimes`, `when` and `while`. I don't want to implement those again. Their expansions must fit the [supported subset](language.md). Creating functions, using `letfn`, and handling exceptions or resources belong outside the region. Calling an external function from inside it is fine.
-
 ## A state machine
 
 If you've used [core.async's `go` macro](https://clojure.org/news/2013/06/28/clojure-clore-async-channels#_go_blocks_and_ioc_threads), you've already used a compiler inside a macro. Its state-machine implementation rewrites the block so a channel operation such as `<!` can park and later resume, preserving the local values and the point where execution should continue.
@@ -98,9 +77,9 @@ If you've used [core.async's `go` macro](https://clojure.org/news/2013/06/28/clo
 
 One way to compile the body is to turn it into a [control-flow graph](https://www.cs.cornell.edu/courses/cs4120/2026sp/notes.html?id=traces), or CFG. Its nodes are _basic blocks_: sequences of operations with one entry point and no internal branches at this level of representation. Each block ends with a _terminator_ that jumps, chooses a successor, returns, or throws. The graph's edges are the possible transfers between blocks.
 
-We can represent this with a single `loop` and `case`. A loop binding named `pc`, our _program counter_, holds the ID of the block to execute. The other bindings carry values between blocks. The `case` is a _dispatcher_: it selects the block named by `pc`.
+We can represent a CFG with a single `loop` and `case`. A loop binding named `pc`, our _program counter_, holds the ID of the block to execute. The other bindings carry values between blocks. The `case` is a _dispatcher_: it selects the block named by `pc`.
 
-Before looking at the compiler's output, here is that idea written by hand:
+Here is a hand-written dispatcher for the preceding example:
 
 ```clojure
 (loop [pc 0 i 0 sum 0]
@@ -115,8 +94,6 @@ Before looking at the compiler's output, here is that idea written by hand:
 ;; => 45
 ```
 
-This is ordinary Clojure. The program counter and state are loop bindings. No mutable reference object is needed to hold `sum`.
-
 A compiler's _backend_ turns its internal description of a program into output code. Blacktide has a dispatcher backend and a _structured_ backend, which reconstructs Clojure loops and conditionals where the graph permits. It chooses between them before emitting Clojure.
 
 The pipeline works through these steps; we'll unpack them below:
@@ -150,7 +127,7 @@ _Lowering_ rewrites a richer construct into simpler operations. For example, ass
 
 Later passes can work with individual writes without having to understand the two assignment forms again. The [language definitions](../src/com/tailrecursion/blacktide/compiler/languages.clj) record that progression: `Sequential` removes `:setq` and adds `:write`; `Assigned` then removes `:psetq`. These are contracts the machinery can use.
 
-Blacktide's [nanopass library](nanopass.md) generates code to walk unchanged node kinds and derives Typed Clojure types from the language descriptions. Development checks verify pass bodies against those contracts. A pass can still produce a beautifully well-typed wrong answer, so we also need tests of the program's behavior.
+Blacktide's [nanopass library](nanopass.md) generates code to walk unchanged node kinds and derives [Typed Clojure](https://github.com/typedclojure/typedclojure) types from the language descriptions. Development checks verify pass bodies against those contracts. A pass can still produce a beautifully well-typed wrong answer, so we also need tests of the program's behavior.
 
 Andy Keep's [Writing a Nanopass Compiler](https://www.youtube.com/watch?v=Os7FE3J-U5Q), from Clojure/conj 2013, introduces the approach with a small Scheme-to-C compiler. His paper with R. Kent Dybvig, [A Nanopass Framework for Commercial Compiler Development](https://andykeep.com/pubs/np-preprint.pdf), describes the framework and its use in Chez Scheme.
 
@@ -219,7 +196,7 @@ The remaining examples use `stages` to run the pipeline and retain intermediate
 
 The helper assumes the form's locals are bound inside `loophole`. It can still call functions in the current namespace. Evaluate the original stage values; `readable` produces display data. Generated names may differ.
 
-I'll walk through the original `dotimes` example to see what the important steps do.
+To keep the intermediate output manageable, let's trace a smaller example: summing the numbers below ten.
 
 ## 1. Macroexpansion