git » blacktide.git » commit f9e37e9

Require explicit labels and configure Blacktide CI

author Alan Dipert
2026-11-06 20:14:07 UTC
committer Alan Dipert
2026-11-06 20:14:07 UTC
parent d7b99f145268bbbf9ac6f7976e3fec958637d1c1

Require explicit labels and configure Blacktide CI

Remove the implicit end exit and reserved label name, update the language
reference and examples, and exercise explicit labels in existing generators.

Use the renamed Blacktide checkout in documentation and test fixtures.
Run Forgejo CI on JDK 17/21/25/27, with pass type checks on 25/27 and a JAR
build on 25. Pin actions and cache Maven dependencies.

.forgejo/workflows/check.yml +43 -0
README.md +3 -2
doc/compiler.md +4 -5
doc/language.md +5 -7
examples/blacktide/algorithms.clj +2 -3
src/com/tailrecursion/blacktide.clj +1 -1
src/com/tailrecursion/blacktide/compiler/lower.clj +1 -5
src/com/tailrecursion/blacktide/compiler/source.clj +0 -2
test/com/tailrecursion/blacktide/aot_test.clj +1 -1
test/com/tailrecursion/blacktide/backend_test.clj +3 -3
test/com/tailrecursion/blacktide/check.clj +1 -1
test/com/tailrecursion/blacktide/generated_test.clj +1 -1
test/com/tailrecursion/blacktide/jumps_test.clj +9 -9
test/com/tailrecursion/blacktide/jvm_fixture.clj +1 -1
test/com/tailrecursion/blacktide/jvm_test.clj +4 -4
test/com/tailrecursion/blacktide/loops_test.clj +1 -1
test/com/tailrecursion/blacktide/source_test.clj +2 -2
test/com/tailrecursion/blacktide/subset_test.clj +15 -15
test/com/tailrecursion/nanopass/assignment_test.clj +6 -10
test/com/tailrecursion/nanopass/clients/assignment.clj +0 -1

diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml
new file mode 100644
index 0000000..52fe86f
--- /dev/null
+++ b/.forgejo/workflows/check.yml
@@ -0,0 +1,43 @@
+name: Check
+
+on:
+  push:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  check:
+    runs-on: debian
+    timeout-minutes: 30
+    strategy:
+      fail-fast: false
+      matrix:
+        java: ['17', '21', '25', '27']
+    steps:
+      - uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
+        with:
+          persist-credentials: false
+      - uses: https://github.com/actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
+        with:
+          distribution: temurin
+          java-version: ${{ matrix.java }}
+      - uses: https://data.forgejo.org/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+        with:
+          path: ~/.m2/repository
+          key: maven-${{ matrix.java }}-${{ hashFiles('deps.edn') }}
+      - name: Install Clojure CLI
+        run: |
+          cd "$RUNNER_TEMP"
+          curl -fsSLO https://download.clojure.org/install/linux-install-1.12.4.1618.sh
+          bash linux-install-1.12.4.1618.sh --prefix "$RUNNER_TEMP/clojure"
+          echo "$RUNNER_TEMP/clojure/bin" >> "$GITHUB_PATH"
+      - name: Test
+        run: |
+          clojure -T:build compile-java
+          clojure -M:test
+      - name: Check pass types
+        if: matrix.java == '25' || matrix.java == '27'
+        run: clojure -M:check
+      - name: Build JAR
+        if: matrix.java == '25'
+        run: clojure -T:build jar
diff --git a/README.md b/README.md
index 6997247..b7d98b8 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Clojure, without mutable cells or a runtime interpreter for local state.
 `setq` assigns pairs sequentially and returns the last value. `psetq` evaluates
 all right-hand sides before assigning and returns nil. Both work in expressions.
 Labels belong to the outer statement sequence; jumps can leave nested statement
-bodies. `(goto end)` exits with nil.
+bodies. Every jump requires an explicit label.
 
 The region supports `let`, destructuring, `if`, `case`, `loop`/`recur`, calls,
 collections and Java interop. Ordinary macros such as `when`, `cond`, `dotimes`
@@ -43,7 +43,7 @@ This is a control-flow tool; it makes no general speedup guarantee.
 Requires Clojure 1.12.6 and JDK 17 or later. From a neighboring checkout:
 
 ```clojure
-{:deps {com.tailrecursion/blacktide {:local/root "../loophole"}}}
+{:deps {com.tailrecursion/blacktide {:local/root "../blacktide"}}}
 ```
 
 Run `clojure -X:deps prep` in the client project to compile the bundled JVM
@@ -63,6 +63,7 @@ clojure -T:build jar         # target/blacktide-0.1.0-SNAPSHOT.jar
 Select a JDK with `JAVA_CMD=/path/to/jdk/bin/java bin/check`. Development targets
 JDK 25/27; 17/21 are compatibility targets. Generators use deterministic seeds,
 independent models and shrinking; failures report their seed and source.
+Forgejo CI tests all four JDKs, checks pass types on 25/27, and builds the JAR.
 
 See the [compiler architecture](doc/compiler.md) and [nanopass guide](doc/nanopass.md)
 for implementation contracts and extension guidance.
diff --git a/doc/compiler.md b/doc/compiler.md
index f2102e5..19b48b1 100644
--- a/doc/compiler.md
+++ b/doc/compiler.md
@@ -18,11 +18,10 @@ contracts. Compiler namespaces and intermediate representations are internal.
 
 `Bound → Classified → Sequential → Assigned → Cases → Collections → Prepared → Ordered → Symbolic → CFG → Machine → Code`.
 
-The outer bindings enter as one lexical scope. `goto end` directly returns nil;
-emission seals the whole result with an Object type boundary. `Native` is the
-analyzed subtree language without DSL operations. The structured backend uses
-`Machine → Planned → Joined → Stored → Compact → Code` and shares graph
-validation with the dispatcher.
+The outer bindings enter as one lexical scope; emission seals the result with an
+Object type boundary. `Native` contains analyzed subtrees without DSL operations.
+The structured backend uses `Machine → Planned → Joined → Stored → Compact → Code`
+and shares graph validation with the dispatcher.
 
 Assignments, normalization and graph construction are pure. Locals become
 lexical bindings and loop parameters, never mutable cells. A generated
diff --git a/doc/language.md b/doc/language.md
index a107484..dfc6e12 100644
--- a/doc/language.md
+++ b/doc/language.md
@@ -33,12 +33,12 @@ Only bindings introduced by `loophole` are assignable. Shadowing with an ordinar
 Labels are simple symbols in the **outer statement sequence**, including nested
 transparent `do` forms after macroexpansion. They cannot appear inside `let`,
 `loop`, conditional arms, operands or initializers. Names are unique within the
-region; `end` is reserved.
+region. Labels evaluate to nil.
 
 `goto` is a statement. It can occur in `if`/`case` arms and `let`/`loop` bodies
 and jump out to an outer label. It is forbidden anywhere within an operand or
-initializer, even a discarded expression nested in `do`. `(goto end)` returns
-nil immediately. A normal final result can be obtained with a final label:
+initializer, even a discarded expression nested in `do`. Every jump requires
+an explicit label in the same region; execution continues after that label.
 
 ```clojure
 (loophole [x 0]
@@ -49,8 +49,6 @@ nil immediately. A normal final result can be obtained with a final label:
   (goto done)
   (setq x 99)
   (label done) x) ; => 3
-
-(loophole [] (goto end) :unreachable) ; => nil
 ```
 
 Backward jumps rerun initializers they encounter. They cannot enter a lexical
@@ -58,8 +56,8 @@ scope or bypass its initialization. These forms are errors:
 
 ```clojure
 (loophole [] (let [] (label inside)))
-(loophole [] (+ 1 (do (goto end) 2)))
-(loophole [x (goto end)] x)
+(loophole [] (+ 1 (do (goto done) 2)) (label done))
+(loophole [x (goto done)] (label done) x)
 ```
 
 ## Clojure subset
diff --git a/examples/blacktide/algorithms.clj b/examples/blacktide/algorithms.clj
index 24a03d4..e7bdc71 100644
--- a/examples/blacktide/algorithms.clj
+++ b/examples/blacktide/algorithms.clj
@@ -33,7 +33,7 @@
 
 (defn decimal-prefix [^String s]
   (loophole [i 0 sign 1 value 0N start 0]
-    (when (empty? s) (goto end))
+    (when (empty? s) (goto done))
     (case (.charAt s 0)
       \- (setq sign -1 i 1)
       \+ (setq i 1)
@@ -47,5 +47,4 @@
             i (inc i)))
     (goto digit)
     (label done)
-    (when (= i start) (goto end))
-    [(* sign value) i]))
+    (when (< start i) [(* sign value) i])))
diff --git a/src/com/tailrecursion/blacktide.clj b/src/com/tailrecursion/blacktide.clj
index ab04ce0..b3e558a 100644
--- a/src/com/tailrecursion/blacktide.clj
+++ b/src/com/tailrecursion/blacktide.clj
@@ -11,7 +11,7 @@
                (errors/site {:form &form :file *file* :line nil :column nil} &form)))
 
 (defmacro goto
-  "Jump from a statement to an outer label; (goto end) returns nil."
+  "Jump from a statement to an explicit outer label."
   [& _]
   (errors/fail :outside-region "goto is only valid inside loophole" {}
                (errors/site {:form &form :file *file* :line nil :column nil} &form)))
diff --git a/src/com/tailrecursion/blacktide/compiler/lower.clj b/src/com/tailrecursion/blacktide/compiler/lower.clj
index 1d812ea..e889868 100644
--- a/src/com/tailrecursion/blacktide/compiler/lower.clj
+++ b/src/com/tailrecursion/blacktide/compiler/lower.clj
@@ -122,11 +122,7 @@
                  builder (assoc builder :labels (assoc (:labels builder) key label))
                  builder (seal builder {:op :jump :target label})]
              [(start builder label) {:op :literal :value nil}]))
-  :goto (let [builder
-              (if (= 'end (:name node))
-                (seal builder {:op :return :value {:op :literal :value nil}})
-                (seal builder {:op :transfer :name (:name node)
-                               :site (:site expr)}))
+  :goto (let [builder (seal builder {:op :transfer :name (:name node) :site (:site expr)})
               [builder next] (fresh-block builder)]
           [(start builder next) {:op :literal :value nil}])
   :do (let [[builder values] (lower-forms builder (:forms node))]
diff --git a/src/com/tailrecursion/blacktide/compiler/source.clj b/src/com/tailrecursion/blacktide/compiler/source.clj
index f7bebcc..53ea460 100644
--- a/src/com/tailrecursion/blacktide/compiler/source.clj
+++ b/src/com/tailrecursion/blacktide/compiler/source.clj
@@ -313,8 +313,6 @@
     (when-not (and (:statement? ctx) (:transfer? ctx) (or (= kind :goto) (:outer? ctx)))
       (errors/fail :marker-position "Labels require the outer body; goto requires a statement outside operands and initializers"
                    {:operator kind :label name} (:site ctx)))
-    (when (and (= kind :label) (= name 'end))
-      (errors/fail :reserved-label "end is reserved" {:label name} (:site ctx)))
     (expression ctx {:hint nil :var-meta {}}
                 (if (= kind :label) {:op :label :name name}
                     {:op :goto :name name}))))
diff --git a/test/com/tailrecursion/blacktide/aot_test.clj b/test/com/tailrecursion/blacktide/aot_test.clj
index 445a334..eac31f7 100644
--- a/test/com/tailrecursion/blacktide/aot_test.clj
+++ b/test/com/tailrecursion/blacktide/aot_test.clj
@@ -13,7 +13,7 @@
     {:exit (.waitFor process) :output output}))
 
 (deftest fresh-aot-and-source-free-use
-  (let [directory (.toFile (java.nio.file.Files/createTempDirectory "loophole-aot-" (make-array java.nio.file.attribute.FileAttribute 0)))
+  (let [directory (.toFile (java.nio.file.Files/createTempDirectory "blacktide-aot-" (make-array java.nio.file.attribute.FileAttribute 0)))
         classpath (System/getProperty "java.class.path")
         dependencies (filter #(str/ends-with? % ".jar") (str/split classpath (re-pattern java.io.File/pathSeparator)))
         compiled (subprocess classpath `(binding [*compile-path* ~(.getPath directory)]
diff --git a/test/com/tailrecursion/blacktide/backend_test.clj b/test/com/tailrecursion/blacktide/backend_test.clj
index 244271f..66ed081 100644
--- a/test/com/tailrecursion/blacktide/backend_test.clj
+++ b/test/com/tailrecursion/blacktide/backend_test.clj
@@ -50,7 +50,7 @@
   (list 'quote (.getJavaClass ^clojure.lang.Compiler$LocalBinding (get &env name))))
 
 (deftest result-type-boundary
-  (doseq [body '[1 2.5 nil false "text" (loop [] 1) (goto end)]
+  (doseq [body '[1 2.5 nil false "text" (loop [] 1) (do (goto end) :dead (label end) 42)]
           mode [:default :dispatcher]]
     (let [form (list 'let ['result (list 'loophole [] body)] '(local-class result))]
       (is (= Object ((compile-program form mode) nil)) (pr-str [body mode])))))
@@ -195,12 +195,12 @@
 (deftest structured-code-preserves-native-validation
   (doseq [invalid '[(Math/abs 1 2 3) (String. 1 2 3 4 5)]]
     (let [form (list 'loophole '[i 0] '(label again) '(setq i (inc i))
-                     '(when (< i 2) (goto again)) '(goto end) invalid)]
+                     '(when (< i 2) (goto again)) '(goto done) invalid '(label done))]
       (doseq [mode [:default :dispatcher]]
         (is (thrown? clojure.lang.Compiler$CompilerException (compile-program form mode))))))
   (let [form '(loophole [i 0]
                 (label again) (setq i (inc i)) (when (< i 2) (goto again))
-                (goto end) (swap! events conj :dead))
+                (goto done) (swap! events conj :dead) (label done))
         events (atom [])]
     (is (= :validation (:reason (selection form))))
     (is (nil? ((compile-program form :default) events)))
diff --git a/test/com/tailrecursion/blacktide/check.clj b/test/com/tailrecursion/blacktide/check.clj
index 9d3b0cf..50f3b29 100644
--- a/test/com/tailrecursion/blacktide/check.clj
+++ b/test/com/tailrecursion/blacktide/check.clj
@@ -58,7 +58,7 @@
 (deftest native-and-recur-boundaries
   (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Native-Node {:op :setq :pairs []} false)
   (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Native-Node
-                  {:op :goto :name 'end} false)
+                  {:op :goto :name 'done} false)
   (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Ordered-Node
                   {:op :recur :target {:id 'loop :bindings []} :args []} true)
   (probes/accepts? 'com.tailrecursion.blacktide.compiler.source-languages/Ordered-Node
diff --git a/test/com/tailrecursion/blacktide/generated_test.clj b/test/com/tailrecursion/blacktide/generated_test.clj
index a4a2756..b92ce7c 100644
--- a/test/com/tailrecursion/blacktide/generated_test.clj
+++ b/test/com/tailrecursion/blacktide/generated_test.clj
@@ -80,7 +80,7 @@
         form `(loophole [~(local 'i) 0 ~(local 'x) ~(initial x) ~(local 'y) ~(initial y)]
                 ~@forms (label ~'done) ~result)]
     (if renamed?
-      (let [renames '{i counter__17 x value__93 y value__94 again start__51 done finish__88}]
+      (let [renames '{i counter__17 x value__93 y value__94 again start__51 done end}]
         (list 'let ['unused :harmless]
               (walk/postwalk #(if (symbol? %) (with-meta (get renames % %) (meta %)) %) form)))
       form)))
diff --git a/test/com/tailrecursion/blacktide/jumps_test.clj b/test/com/tailrecursion/blacktide/jumps_test.clj
index 60e04c8..fedcb7d 100644
--- a/test/com/tailrecursion/blacktide/jumps_test.clj
+++ b/test/com/tailrecursion/blacktide/jumps_test.clj
@@ -11,8 +11,8 @@
 (defn record! [events tag value] (swap! events conj tag) value)
 
 (deftest basic-and-conditional-transfers
-  (is (= 1 (loophole [x 1] (goto done) (setq x 9) (label done) x)))
-  (is (nil? (loophole [] (goto end) :unreachable)))
+  (is (= 1 (loophole [x 1] (goto end) (setq x 9) (label end) x)))
+  (is (nil? (loophole [] (goto done) :unreachable (label done))))
   (is (nil? (loophole [] (label first) (label last))))
   (is (= 0 (loophole [n 3] (label again) (setq n (step n)) (when (more? n) (goto again)) n))))
 
@@ -27,20 +27,20 @@
 
 (deftest invalid-transfers-even-in-dead-source
   (doseq [[body kind] [['(goto missing) :undefined-label]
+                       ['(goto end) :undefined-label]
                        ['(do (label x) (label x)) :duplicate-label]
-                       ['(label end) :reserved-label]
                        ['(label :keyword) :invalid-marker]
                        ['(goto qualified/name) :invalid-marker]
                        ['(vector (label x)) :marker-position]
                        ['(let [] (label x)) :marker-position]
                        ['(if true (label x) nil) :marker-position]
                        ['(case 0 0 (label x) nil) :marker-position]
-                       ['(vector (do (goto end) 1)) :marker-position]
-                       ['(let [x (do (goto end) 1)] x) :marker-position]
-                       ['(setq x (do (goto end) 1)) :marker-position]
+                       ['(vector (do (goto done) 1)) :marker-position]
+                       ['(let [x (do (goto done) 1)] x) :marker-position]
+                       ['(setq x (do (goto done) 1)) :marker-position]
                        ['(loophole [] 1) :unsupported-form]]
-          prefix [[] ['(goto end)]]]
-    (is (= kind (:kind (source/rejection (list* 'loophole '[x 0] (concat prefix [body])))))
+          prefix [[] ['(goto done)]]]
+    (is (= kind (:kind (source/rejection (list* 'loophole '[x 0] (concat prefix [body '(label done)])))))
         (pr-str [prefix body]))))
 
 (defn sum-through [input]
@@ -105,7 +105,7 @@
                                               :yes (.nextInt random 3) :no (.nextInt random 3))))
             budget (.nextInt random 14) value (- (.nextInt random 41) 20)
             expected (reference-machine table budget value)]
-        (doseq [[labels wrapped?] [['[a b c] false] ['[renamed-a renamed-b renamed-c] true]]]
+        (doseq [[labels wrapped?] [['[end b c] false] ['[renamed-a renamed-b renamed-c] true]]]
           (let [actual (execute-machine table budget value labels wrapped?)]
             (is (= expected actual)
                 (when (not= expected actual)
diff --git a/test/com/tailrecursion/blacktide/jvm_fixture.clj b/test/com/tailrecursion/blacktide/jvm_fixture.clj
index d96a53e..874a34d 100644
--- a/test/com/tailrecursion/blacktide/jvm_fixture.clj
+++ b/test/com/tailrecursion/blacktide/jvm_fixture.clj
@@ -3,7 +3,7 @@
   (:require [clojure.java.io :as io]))
 
 (def fixture-class
-  (let [directory (.toFile (java.nio.file.Files/createTempDirectory "loophole-jvm-fixture-"
+  (let [directory (.toFile (java.nio.file.Files/createTempDirectory "blacktide-jvm-fixture-"
                            (make-array java.nio.file.attribute.FileAttribute 0)))
         source (io/file directory "HostFixture.java")
         compiler (javax.tools.ToolProvider/getSystemJavaCompiler)]
diff --git a/test/com/tailrecursion/blacktide/jvm_test.clj b/test/com/tailrecursion/blacktide/jvm_test.clj
index 8c6fba0..390faa3 100644
--- a/test/com/tailrecursion/blacktide/jvm_test.clj
+++ b/test/com/tailrecursion/blacktide/jvm_test.clj
@@ -221,13 +221,13 @@
           wrap [identity #(list 'do %)]]
     (let [body (wrap form)
           expected (outcome body)
-          actual (outcome (list 'loophole [] '(goto end) body))]
+          actual (outcome (list 'loophole [] '(goto done) body '(label done)))]
       (is (:error expected) (pr-str {:form body :native expected}))
       (is (= (:error expected) (:error actual)) (pr-str {:form body :native expected :actual actual}))))
   ;; Instance reflection reports a missing overload only when executed.
-  (is (nil? (loophole [] (goto end) (.substring "abc" 1 2 3))))
+  (is (nil? (loophole [] (goto done) (.substring "abc" 1 2 3) (label done))))
   (let [events (atom [])]
-    (is (nil? (loophole [] (goto end)
+    (is (nil? (loophole [] (goto done)
                 (swap! events conj :expression)
-                (identity (swap! events conj :after)))))
+                (identity (swap! events conj :after)) (label done))))
     (is (empty? @events))))
diff --git a/test/com/tailrecursion/blacktide/loops_test.clj b/test/com/tailrecursion/blacktide/loops_test.clj
index 812187e..9c77a66 100644
--- a/test/com/tailrecursion/blacktide/loops_test.clj
+++ b/test/com/tailrecursion/blacktide/loops_test.clj
@@ -124,7 +124,7 @@
            ['(loophole [x nil] (loop [n 1] (setq x n) (recur (short n)))) :invalid-recur-type]
            ['(loophole [] (goto body) (loop [] (label body))) :marker-position]
            ['(loophole [] (loop [] (goto other)) (loop [] (label other))) :marker-position]
-           ['(loophole [] (goto end) (loop [] (recur 1))) :recur-arity]]]
+           ['(loophole [] (goto done) (loop [] (recur 1)) (label done)) :recur-arity]]]
     (is (= kind (:kind (source/rejection form))) (pr-str form))))
 
 (deftest illegal-primitive-recur-conversions
diff --git a/test/com/tailrecursion/blacktide/source_test.clj b/test/com/tailrecursion/blacktide/source_test.clj
index 2c64535..5869e03 100644
--- a/test/com/tailrecursion/blacktide/source_test.clj
+++ b/test/com/tailrecursion/blacktide/source_test.clj
@@ -168,8 +168,8 @@
 
 (deftest diagnostic-context-survives-expansion-and-native-boundaries
   (doseq [body '[(case)]
-          prefix [[] ['(goto end)]]]
-    (let [form (with-meta (list* 'loophole [] (concat prefix [body])) {:line 123 :column 9})
+          prefix [[] ['(goto done)]]]
+    (let [form (with-meta (list* 'loophole [] (concat prefix [body '(label done)])) {:line 123 :column 9})
           error (binding [*file* "diagnostic-example.clj"] (rejection form))]
       (is (= :invalid-case (:kind error)))
       (is (= '(case) (:form error)))
diff --git a/test/com/tailrecursion/blacktide/subset_test.clj b/test/com/tailrecursion/blacktide/subset_test.clj
index 12719e3..6b4db4a 100644
--- a/test/com/tailrecursion/blacktide/subset_test.clj
+++ b/test/com/tailrecursion/blacktide/subset_test.clj
@@ -16,27 +16,27 @@
                 (binding [*print-length* 1] 2) (locking (Object.) 1)
                 (with-open [r (java.io.StringReader. "")] 1) (with-redefs [identity vector] 1)
                 (com.tailrecursion.blacktide.subset-test/hidden-function)]
-          prefix [[] ['(goto end)]]]
+          prefix [[] ['(goto done)]]]
     (is (= :unsupported-form
-           (:kind (source/rejection (list* 'loophole '[x 0] (concat prefix [body])))))
+           (:kind (source/rejection (list* 'loophole '[x 0] (concat prefix [body '(label done)])))))
         (pr-str [prefix body]))))
 
 (deftest operands-cannot-hide-transfers
-  (doseq [body '[(identity (do (goto end) 1))
-                ((do (goto end) identity) 1)
-                (if (do (goto end) true) 1 2)
-                (case (do (goto end) 1) 1 2 3)
-                (let [a (do (goto end) 1)] a)
-                (loop [a (do (goto end) 1)] a)
-                (loop [a 0] (recur (do (goto end) 1)))
-                (setq x (case 1 1 (goto end) 2))
-                [(let [] (goto end) 1)]
-                (identity (loop [] (goto end)))
-                ^{:note (do (goto end) 1)} [1]
+  (doseq [body '[(identity (do (goto done) 1))
+                ((do (goto done) identity) 1)
+                (if (do (goto done) true) 1 2)
+                (case (do (goto done) 1) 1 2 3)
+                (let [a (do (goto done) 1)] a)
+                (loop [a (do (goto done) 1)] a)
+                (loop [a 0] (recur (do (goto done) 1)))
+                (setq x (case 1 1 (goto done) 2))
+                [(let [] (goto done) 1)]
+                (identity (loop [] (goto done)))
+                ^{:note (do (goto done) 1)} [1]
                 (let [] (com.tailrecursion.blacktide.subset-test/hidden-label))]]
-    (is (= :marker-position (:kind (source/rejection (list 'loophole '[x 0] body)))) (pr-str body)))
+    (is (= :marker-position (:kind (source/rejection (list 'loophole '[x 0] body '(label done))))) (pr-str body)))
   (is (= :ok (loophole [] (do (label a) (do (goto b))) :wrong (label b) :ok)))
-  (is (= '(fn [] (goto end)) (loophole [] '(fn [] (goto end))))))
+  (is (= '(fn [] (goto missing)) (loophole [] '(fn [] (goto missing))))))
 
 (deftest caller-functions-and-dynamic-extent
   (let [events (atom []) helper (fn [x] (try (inc x) (finally (swap! events conj :helper))))]
diff --git a/test/com/tailrecursion/nanopass/assignment_test.clj b/test/com/tailrecursion/nanopass/assignment_test.clj
index 6fa625c..e7d4ab1 100644
--- a/test/com/tailrecursion/nanopass/assignment_test.clj
+++ b/test/com/tailrecursion/nanopass/assignment_test.clj
@@ -27,7 +27,6 @@
             (run (if test (:then form) (:else form)) state))
       :event (let [[value state] (run (:value form) state)]
                [value (update state :trace conj [(:tag form) value])])
-      :exit (throw (ex-info "Region exit" {:status :exit :state state}))
       :throw (throw (ex-info "Source exception" {:status :throw :tag (:tag form) :state state}))
       ;; Source assignments define their semantics directly, independent of
       ;; the pass's generated binding tree and the output's write primitive.
@@ -94,18 +93,15 @@
           (is (= expected (execute (lower/lower source) {'x 1 'y 2}))))))))
 
 (deftest abandoned-writes
-  (doseq [op [:setq :psetq]
-          transfer [{:op :exit} {:op :throw :tag :failed}]
-          nested? [false true]]
+  (doseq [op [:setq :psetq] nested? [false true]]
     (let [source (batch op 'x (event :first (lit 10))
                           'y (sequence-of
                                (if nested? (batch :setq 'x (lit 9)) (lit nil))
-                               transfer))
-          expected (cond-> {:status (if (= :exit (:op transfer)) :exit :throw)
-                            :vars {'x (cond nested? 9 (= op :setq) 10 :else 1) 'y 2}
-                            :trace [[:first 10]]}
-                     (= :throw (:op transfer)) (assoc :tag :failed))]
-      (testing (pr-str [op transfer nested?])
+                               {:op :throw :tag :failed}))
+          expected {:status :throw :tag :failed
+                    :vars {'x (cond nested? 9 (= op :setq) 10 :else 1) 'y 2}
+                    :trace [[:first 10]]}]
+      (testing (pr-str [op nested?])
         (is (= expected (execute source {'x 1 'y 2})))
         (is (= expected (execute (lower/lower source) {'x 1 'y 2})))))))
 
diff --git a/test/com/tailrecursion/nanopass/clients/assignment.clj b/test/com/tailrecursion/nanopass/clients/assignment.clj
index c6f13d1..1090a9f 100644
--- a/test/com/tailrecursion/nanopass/clients/assignment.clj
+++ b/test/com/tailrecursion/nanopass/clients/assignment.clj
@@ -7,7 +7,6 @@
 (np/deflanguage Source
   {Expr (t/U (t/HMap :mandatory {:op ':const :value t/Any} :complete? true)
              (t/HMap :mandatory {:op ':read :binding t/Sym} :complete? true)
-             (t/HMap :mandatory {:op ':exit} :complete? true)
              (t/HMap :mandatory {:op ':throw :tag t/Keyword} :complete? true)
              (t/HMap :mandatory {:op ':add :left Expr :right Expr} :complete? true)
              (t/HMap :mandatory {:op ':do :forms (t/Vec Expr)} :complete? true)