git » homepage.git » commit fb4bc13

Shorten the mutable-locals introduction and ease the tone

author Alan Dipert
2026-11-11 01:51:53 UTC
committer Alan Dipert
2026-11-11 01:51:53 UTC
parent 5eae3bd76561002077a495ebfca1589e8effed6f

Shorten the mutable-locals introduction and ease the tone

md/_MutableLocals.md +11 -15

diff --git a/md/_MutableLocals.md b/md/_MutableLocals.md
index 7076d1e..84dabd5 100644
--- a/md/_MutableLocals.md
+++ b/md/_MutableLocals.md
@@ -1,10 +1,10 @@
 # Mutable Locals in Clojure, the Hard Way
 
-Hey, so, I've been thinking about mutable local variables in Clojure. Is that OK? Hear me out.
+I've been thinking about mutable local variables in Clojure. Hear me out.
 
-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.
+I like Clojure's immutable values. But sometimes I'm reading an algorithm in pseudocode, Java or C, and I'd like to port it without translating its local variables into atoms or `loop`/`recur`.
 
-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.
+For example, this counts inversions in a vector: pairs where an earlier element is greater than a later one.
 
 ```clojure
 (require '[com.tailrecursion.blacktide :refer [loophole setq label goto]])
@@ -21,15 +21,11 @@ Here's an example of the kind of stuff I'd like to write on occasion. This funct
 ;; => 4
 ```
 
-Nothing crazy, just setting a happy little variable `n` inside nested loops. I'm still here, by the way. Nothing terrible happened.
+Clojure already updates [JVM local-variable slots](https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-2.html#jvms-2.6.1) to implement `loop` and `recur`. We can use the same mechanism for our own variables.
 
-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?
+[`recur`](https://clojure.org/reference/special_forms#recur) evaluates its arguments, rebinds the loop locals, and starts the loop body again. [Clojure's compiler](https://github.com/clojure/clojure/blob/clojure-1.12.6/src/jvm/clojure/lang/Compiler.java#L7156-L7227) emits local-variable stores and a jump. We can write assignments and jumps where we want them, then compile them into Clojure that does the updating through `recur`.
 
-And that gives us a way in. [`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.
-
-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 inside a macro. 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.
+Blacktide's `loophole` macro does this for a subset of Clojure. It's a compiler inside a macro, with no atoms or changes to Clojure's compiler. Mutable locals to the people.
 
 ## Compiling to a state machine
 
@@ -121,7 +117,7 @@ So far, we've used `setq` to update a variable and discarded its result. But `se
 ;; => 30
 ```
 
-The first argument to `+` is 10; the second is 20. If a transformation performs both writes and then reads `x` twice, it produces a very convincing 40.
+The first argument to `+` is 10; the second is 20. If a transformation performs both writes and then reads `x` twice, it produces 40.
 
 The first argument therefore needs a name of its own. The essential transformation looks like this, still written in the source language:
 
@@ -139,7 +135,7 @@ Introducing those temporary names brings another obligation: each [binding](http
 
 ## Organizing the compiler with nanopass
 
-We've now expanded macros, preserved evaluation order, kept bindings distinct, and connected blocks. Combining all of that in one enormous transformation would make for an exciting debugging experience.
+We've now expanded macros, preserved evaluation order, kept bindings distinct, and connected blocks. Doing all of that in one transformation would make it hard to see which step went wrong.
 
 To keep it manageable, I use [nanopass](https://andykeep.com/pubs/np-preprint.pdf), the approach Andrew Keep and R. Kent Dybvig used to rebuild Chez Scheme's compiler. Each pass performs a small transformation between explicitly defined intermediate languages. Keep's [Writing a Nanopass Compiler](https://www.youtube.com/watch?v=Os7FE3J-U5Q), from Clojure/conj, is a good introduction.
 
@@ -147,13 +143,13 @@ Saving operands is one of those passes. Its input language allows expressions in
 
 These languages describe trees of immutable Clojure maps and vectors. The [nanopass machinery](https://tailrecursion.com/repos/r/blacktide.git/b/master/t/doc/f=nanopass.md.html) generates traversal code for unchanged forms and derives [Typed Clojure](https://github.com/typedclojure/typedclojure) types from the language descriptions. The type checker can reject a pass that leaves an assignment in a call argument. It runs during development, without being loaded when somebody uses `loophole`.
 
-Naturally, a pass can still produce a beautifully well-typed wrong answer. Saving the wrong value for `left` could give us that convincing 40 again.
+A pass can still produce a well-typed wrong answer. Saving the wrong value for `left` could give us 40 again.
 
 So our [generative tests](https://tailrecursion.com/repos/r/blacktide.git/b/master/t/test/com/tailrecursion/blacktide/f=generated_test.clj.html) build valid, bounded programs combining assignments, branches, loops, and jumps. We compare their results and recorded execution traces with an independent state model, then repeat the comparison with renamed locals, different control-flow formulations, and different ways of emitting the final Clojure. When a case fails, the generator shrinks it to a smaller valid counterexample.
 
 ## Turning the graph back into a loop
 
-The `case` machine gives us a general way to execute the graph. But look at the graph for our sum: test the index, update the sum, go back to the test. We have painstakingly described a loop using numbered blocks. Perhaps we could, in fact, just emit a loop?
+The `case` machine gives us a general way to execute the graph. But look at the graph for our sum: test the index, update the sum, go back to the test. We've described a loop using numbered blocks. Perhaps we could just emit a loop?
 
 That removes the need to carry a program counter and repeatedly ask `case` which block comes next. The test becomes an `if`, the backward edge becomes a `recur`, and the exit returns the sum. Even our added `goto` fits: reaching five simply takes another exit from the loop.
 
@@ -179,7 +175,7 @@ Both loop variables are primitive longs. A value-analysis pass proves that each
 
 The [`RT.box(Object)`](https://github.com/clojure/clojure/blob/clojure-1.12.6/src/jvm/clojure/lang/RT.java#L1037-L1039) calls are at the exits: Clojure boxes the final long, and `RT.box(Object)` returns that object unchanged. The loop's arithmetic stays unboxed. Putting this boundary around the whole loop would make Clojure generate an extra function to evaluate it.
 
-When the index reaches five, the inner `if` returns the sum: 10. That was our `goto`. Having gone to considerable trouble to accommodate it, we have compiled it out of existence.
+When the index reaches five, the inner `if` returns the sum: 10. That was our `goto`. We've compiled it out of existence.
 
 ## Back to the inversion counter