Dafny–Clojure JVM Interop: Verified Transforms on the JVM

Table of Contents

;; Dafny-verified codecs called from the Clojure REPL on FreeBSD 15.0
;; JAR compiled with: dafny build --target:java --unicode-char:false Involutions.dfy

user=> (import [dafny DafnySequence] [_System __default])
user=> (defn ->ds [s] (DafnySequence/asString s))
user=> (defn <-ds [ds] (.verbatimString ds))

user=> (<-ds (_System.__default/Rot13 (->ds "Hello")))
"Uryyb"

user=> (<-ds (_System.__default/Rot13 (->ds "Uryyb")))
"Hello"          ; involution: proof compiled into the JAR

user=> (<-ds (_System.__default/Atbash (->ds "HELLO")))
"SVOOL"

user=> (<-ds (_System.__default/Jump5 (->ds "12345")))
"67890"

1. Overview

The transform codec pipeline has 24 codecs in four patterns (involution, bijection, parameterized, surjection), Lean 4 proofs of their algebraic properties (Codec.roundtrip, encode_inj), malli refinement schemas, and test.check samplers. None of these produce runnable JVM artifacts from the proofs themselves. There is a translation gap: Lean proves properties about mathematical functions, Clojure implements those functions independently, and the correspondence between the two is maintained by human discipline.

Dafny compiles verified programs to Java, JavaScript, C#, Go, and C++. A Dafny function is simultaneously a specification (preconditions and postconditions statically verified) and an executable (compiled to a target language). The proof and the runtime are the same artifact.

This note plans the integration: compile Dafny-verified codec specifications to Java, then call those compiled implementations from Clojure via standard JVM interop. The external Dafny verification at aygp-dr/transform-tool-dafny already provides Codec.dfy, Pipeline.dfy, Proofs.dfy, and CaesarProof.dfy with nine identified spec gaps — this work picks up from that foundation.

1.1. Current verification stack

Layer Artifact Proves Runs
Lean 4 spec/transform-codecs.lean Codec.roundtrip No
malli src/.../transform/spec.cljc Domain refinement REPL
test.check test/.../spec_test.cljc Sampled round-trip CI
Dafny->Java spec/dafny/*.dfy -> .jar Compiled round-trip JVM

The fourth layer closes the gap between proof and execution. Where Lean 4 proves Codec.roundtrip : forall a, decode (encode a) = a as a dependent record that never runs, the Dafny layer compiles the same statement into Java bytecode where decode and encode are real methods with the proof erased at compile time.

2. Environment

uname -a
FreeBSD nexus 15.0-RELEASE FreeBSD 15.0-RELEASE releng/15.0-n280995-7aedc8de6446 GENERIC amd64
java -version 2>&1
java version "21.0.8" 2025-07-15 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 21.0.8+12.1 (build 21.0.8+12-LTS-jvmci-23.1-b72)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.8+12.1 (build 21.0.8+12-LTS-jvmci-23.1-b72, mixed mode, sharing)
clj --version
Clojure CLI version 1.12.0.1488
bb --version
babashka v1.12.206
export PATH="$HOME/.dotnet/tools:$PATH"
dafny --version 2>&1
4.11.0+fcb2042d6d043a2634f0854338c08feeaaaf4ae2
dotnet --version 2>&1
9.0.115

Dafny 4.11.0 runs natively on FreeBSD via the dotnet-9.0.14 port. No cross-compilation needed. See Dafny on FreeBSD 15.0 for installation details.

GraalVM 21 is notable: the Dafny-compiled Java can be ahead-of-time compiled to a native binary via native-image, giving verified codecs with no JVM startup cost.

3. Existing Dafny Verification

The aygp-dr/transform-tool-dafny repository contains four Dafny source files that model the transform codec registry and prove properties about it. This section documents what exists, what it proves, and where the spec gaps are.

3.1. Codec.dfy: Algebraic model

Defines the four codec patterns as a discriminated union:

datatype CodecEntry =
  | CInvolution(inv: Involution)
  | CBijection(bij: Bijection)
  | CParameterized(par: Parameterized)
  | CSurjection(sur: Surjection)

The Involution record carries a single function fn: String -> String and the property IsInvolution(inv) : forall x :: inv.fn(inv.fn(x)) = x. The Bijection carries encode and decode with RoundTrip(b) : forall x | ValidEncodeDomain(b, x) :: b.decode(b.encode(x)) = x.

Concrete implementations and proofs for identity, reverse, and rot13 are included. The ReverseConcat lemma proves the anti-homomorphism property: Reverse(a + b) = Reverse(b) + Reverse(a)=.

3.2. Pipeline.dfy: Execution and reversibility

Models the pipeline as a sequence of Step records threaded left-to-right. Key definitions:

  • ResolveStep: pattern-matches on CodecEntry to select the correct function for a given direction
  • Execute: recursive pipeline execution with halt-on-first-error semantics
  • PipelineIsReversible: every step's codec is reversible and both directions resolve
  • ReversePipelineCorrect: reverses step order and flips directions, with involutions keeping Encode (direction irrelevant)

The core round-trip theorem is stated but not yet proved:

lemma RoundTripTheorem(
  registry: Registry, steps: seq<Step>, input: String
)
  requires PipelineIsReversible(registry, steps)
  requires RegistrySemanticallCorrect(registry)
  ensures Execute(registry, ReversePipelineCorrect(registry, steps),
    Execute(registry, steps, input).finalValue).finalValue == input
{
  assume false; // placeholder --- marks an open obligation
}

The assume false is honest: it marks the proof as incomplete. The theorem is blocked on resolving spec gaps SG-2 (domain predicates) and SG-7 (structural vs. semantic reversibility).

3.3. CaesarProof.dfy: Parameterized codec deep-dive

Three claims proved:

  1. Decode is shift (26-N): CaesarDecode(x, n) = RotN(x, (26-n) % 26)= — by definition, but the proof exposes an edge case at N=0 where (26-0) % 26 = 0, not 26.
  2. N=13 is the unique fixed point: For N in [1,25], N=13 is the only value where CaesarEncode(x, N) = CaesarDecode(x, N)= for all x. FixedPointCharacterization proves EncodeSameAsDecode(n) <==> (n = 0 || n = 13). The spec's claim of "uniqueness" is imprecise about N=0.
  3. Round-trip: CaesarDecode(CaesarEncode(x, N), N) = x= for all x and all N in [0,25]. The proof reduces to the modular arithmetic lemma ShiftAndUnshift(a, n): (a + n + (26-n)) % 26 = a=.

Test vector verification: the spec's caesar:3:d on "HW WX EUXWH" decodes to "ET TU BRUTE". Each character shift is checked individually.

3.4. Nine spec gaps

The external Dafny verification identified nine specification gaps. These are the boundary of what can be formally proved without spec clarification:

ID Gap Blocked proofs
SG-1 Character model undefined (UTF-16 vs scalar) base64, all string codecs
SG-2 Codec domain predicates missing ipv4-int, hex:d, hamming:d
SG-3 Caesar arg validation and default undefined Parameterized round-trip
SG-4 corrupt is non-deterministic, not a function Pipeline purity
SG-5 rot13 case handling not specified rot13 totality proof
SG-6 Decode on surjection: error or no-op? Pipeline error semantics
SG-7 Structural vs. semantic reversibility Pipeline round-trip theorem
SG-8 Floating-point equality for temperature codecs f-to-c, c-to-f round-trip
SG-9 Surjection codomain undefined sort surjectivity proof

SG-7 is the most architecturally significant. The Clojure registry uses a structural flag (:inverse? true) that says nothing about whether decode actually inverts encode. A codec could pass the structural check and still have a broken implementation. The Dafny model separates these:

predicate IsReversible(e: CodecEntry)        // structural: has the fields
predicate SemanticallReversible(e: CodecEntry) // semantic: round-trip holds

The pipeline round-trip theorem requires both.

3.5. Composition properties

Proofs.dfy establishes that the composition of two involutions is an involution if and only if they commute. This is a result the Lean 4 specs do not prove (they prove pipeline composition via Equiv.symm.trans but do not address commutativity).

The CommutingInvolutionsCompose lemma:

f(g(f(g(x))))
  = f(f(g(g(x))))   [by commutativity: g(f(y)) = f(g(y))]
  = g(g(x))          [by f involution]
  = x                [by g involution]

In the codec pipeline, this means rot13+atbash+rot13+atbash is identity if and only if rot13 and atbash commute. They do not in general (both operate on letters but with different mappings), so the composition is not an involution — it is a bijection whose inverse is atbash+rot13+atbash+rot13 (reverse the chain).

4. Dafny Compilation to Java

4.1. How Dafny targets the JVM

dafny build --target:java produces Java source files and a Gradle build script. The output directory contains:

out/
  src/main/java/
    Involutions_Compile/        # one Java class per Dafny module
      __default.java            # top-level functions
      Codec.java                # datatype → Java class
    dafny/                      # runtime library (always included)
      DafnySequence.java
      DafnySet.java
      DafnyMap.java
      ...
  build.gradle

4.2. Dafny Java runtime type mappings

Dafny type Java runtime class Notes
string DafnySequence<Character> Alias for seq<char>
seq<T> DafnySequence<T> Immutable, structural equality
set<T> DafnySet<T> Immutable
map<K,V> DafnyMap<K,V> Immutable
multiset DafnyMultiset<T> Bag semantics
int java.math.BigInteger Unbounded by default
real dafny.BigRational Exact rationals
char char (default) or int --unicode-char switches to int
bool boolean Direct
datatype Java class with subclasses One subclass per constructor
array<T> dafny.Array<T> Wrapper around native arrays

4.3. The DafnySequence API

The critical bridge type. Key methods from the Java runtime source:

// Factory: Java String → Dafny string
static DafnySequence<Character> asString(String s)
static DafnySequence<CodePoint> asUnicodeString(String s)

// Extraction: Dafny string → Java String
String verbatimString()

// Core operations
T select(int i)
int length()
DafnySequence<T> subsequence(int lo, int hi)
DafnySequence<T> drop(int lo)
DafnySequence<T> take(int hi)
static <T> DafnySequence<T> concatenate(
  DafnySequence<? extends T> a,
  DafnySequence<? extends T> b)

// Conversion
Array<T> toArray()
static byte[] toByteArray(DafnySequence<Byte> seq)
static int[] toIntArray(DafnySequence<Integer> seq)

// Predicates
boolean isPrefixOf(DafnySequence<U> other)
boolean contains(Object t)
Iterator<T> iterator()

The asString / verbatimString pair is the interop seam. Every codec function in the Dafny output operates on DafnySequence<Character>; the Clojure bridge converts to/from java.lang.String at the boundary.

4.4. The {:extern} mechanism

{:extern} declares that a method or module is implemented outside Dafny. For Java targets, the arguments map to package and class names:

module {:extern "wal_sh.tools.rot13"} Rot13Bridge {
  method {:extern "wal_sh.tools.rot13.core", "rot13"}
    Rot13(s: string) returns (r: string)
    ensures |r| == |s|
    ensures forall i :: 0 <= i < |s| ==>
      (('A' <= s[i] <= 'Z' || 'a' <= s[i] <= 'z') ==>
        r[i] == RotChar(s[i], 13))
}

Dafny verifies all callers of Rot13 against the specification. The implementation itself is trusted — it comes from the Clojure AOT-compiled .class file. This is the seam where verified callers meet unverified (but tested) implementations.

4.5. Ghost erasure

Dafny distinguishes ghost code (used only in proofs) from compiled code. At the Java target:

  • ghost function: erased entirely — no Java class generated
  • function: compiled to a static Java method (Dafny 4.x; was function method in 3.x)
  • lemma: erased — the proof obligation is discharged at verify time
  • method: compiled to a Java method
  • predicate: erased if ghost, compiled if used in runtime code
  • requires / ensures: erased — they are checked statically, not at runtime

This means the compiled JAR contains only the executable codec functions. The proofs that those functions are correct are verified once at compile time and then gone. The JAR is as lean as hand-written Java.

5. Interop Architecture

Three directions, in order of value:

5.1. Direction 1: Dafny-verified codec called from Clojure

The primary use case. Dafny proves the round-trip property at compile time. The compiled Java class is a normal JVM class. Clojure calls it via interop.

;; After adding the Dafny-compiled JAR to the classpath:
(import '[dafny DafnySequence])

;; Bridge: String <-> DafnySequence<Character>
(defn ->dafny-string [^String s]
  (DafnySequence/asString s))

(defn <-dafny-string [dseq]
  (.verbatimString dseq))

;; Dafny `function Rot13(s: string): string` compiles to:
;;   public static DafnySequence<Character> Rot13(DafnySequence<Character> s)
;; in the class Involutions_Compile.__default

(defn verified-rot13 [s]
  (-> s
      ->dafny-string
      Involutions_Compile.__default/Rot13
      <-dafny-string))

;;=> (verified-rot13 "hello")
;;=> "uryyb"
;;=> (verified-rot13 (verified-rot13 "hello"))
;;=> "hello"  ; involution — proved by Rot13StringInvolutive lemma

The threading macro mirrors the pipeline's own threading model. Each codec call is ->dafny-string -> Dafny_method -> <-dafny-string.

5.2. Direction 2: Clojure primitives satisfying Dafny {:extern}

The inverse direction. Dafny declares a specification with {:extern}. Clojure AOT-compiles its codec namespace to .class files. Those classes go on the classpath and satisfy the extern declaration.

;; AOT-compile the codec namespace to .class files
(binding [*compile-path* "target/classes"]
  (compile 'wal-sh.tools.rot13.core))

Dafny verifies callers against the declared spec:

method {:extern "wal_sh.tools.rot13.core", "rot13"}
  ClojureRot13(s: string) returns (r: string)
  ensures |r| == |s|
  ensures forall i :: 0 <= i < |s| ==>
    r[i] == Rot13Char(s[i])

// Dafny can now use ClojureRot13 in other verified code:
method Pipeline(input: string) returns (output: string)
  ensures output == Rot13(Rot13(input))  // proved via the spec
{
  var mid := ClojureRot13(input);
  output := ClojureRot13(mid);
}

This is more fragile than Direction 1 because Dafny trusts the extern. If the Clojure implementation has a bug, the proof is unsound. The value is in verifying composition: callers of the extern are checked against the declared contract.

5.3. Direction 3: Shared JVM equivalence harness

Both implementations run on the same JVM. A test.check property asserts they produce identical output:

(ns wal-sh.tools.transform.dafny-equiv-test
  (:require [clojure.test.check.clojure-test :refer [defspec]]
            [clojure.test.check.generators :as gen]
            [clojure.test.check.properties :as prop]
            [wal-sh.tools.transform.core :as core]
            [wal-sh.tools.transform.dafny-bridge :as bridge]))

(defspec rot13-equivalence 1000
  (prop/for-all [s gen/string-ascii]
    (= (core/rot13 s)
       (bridge/verified-rot13 s))))

(defspec jump5-equivalence 1000
  (prop/for-all [s gen/string-ascii]
    (= ((:fn (core/codec-by-id :jump5)) s)
       (bridge/verified-jump5 s))))

(defspec atbash-equivalence 1000
  (prop/for-all [s gen/string-ascii]
    (= ((:fn (core/codec-by-id :atbash)) s)
       (bridge/verified-atbash s))))

The Dafny version carries a proof. The Clojure version carries 1000 sampled witnesses. Any divergence is a bug in the bridge layer, not in either implementation. This is the strongest assurance: proof AND empirical testing AND cross-implementation agreement.

6. Type Mapping: Dafny to Clojure

The bridge layer converts between Dafny's Java runtime types and Clojure's native types at the interop boundary.

Dafny (Java) Clojure Conversion
DafnySequence<Character> String .verbatimString / asString
DafnySequence<T> vector (vec (.toArrayList seq))
DafnySet<T> set (into #{} (.iterator set))
DafnyMap<K,V> map iterate entries -> (into {} ...)
BigInteger long / bigint .longValueExact / direct
BigRational ratio numerator/denominator -> Ratio
boolean boolean identity
CodePoint char / int .value()
Codec (datatype) map destructure via .is_CInvolution etc.

For the codec pipeline, only the String conversion matters. Every codec operates on string (which is seq<char> which is DafnySequence<Character>). The bridge is exactly two methods wide:

(def ->dafny  DafnySequence/asString)     ;; String -> DafnySequence<Character>
(def <-dafny  #(.verbatimString %))       ;; DafnySequence<Character> -> String

6.1. Performance considerations

DafnySequence is immutable and backed by an array. asString copies the input string's char[] into a new array. verbatimString copies back. For a pipeline of N steps, the naive bridge performs 2N copies.

Optimization: keep the value in DafnySequence form throughout the pipeline and convert only at the boundary:

(defn thread-verified [input steps]
  (let [dafny-input (->dafny-string input)]
    (<-dafny-string
      (reduce (fn [acc step-fn] (step-fn acc))
              dafny-input
              steps))))

This reduces to 2 copies total regardless of pipeline length.

7. The Clojure Bridge Namespace

src/wal_sh/tools/transform/dafny_bridge.clj (JVM-only, not .cljc):

(ns wal-sh.tools.transform.dafny-bridge
  "Bridge between Dafny-compiled verified codecs and the Clojure transform
   pipeline. JVM-only: the Dafny Java runtime is not available in ClJS."
  (:import [dafny DafnySequence]
           [Involutions_Compile __default]))

(defn ->dafny-string [^String s]
  (DafnySequence/asString s))

(defn <-dafny-string [dseq]
  (.verbatimString dseq))

(defn wrap-verified-involution
  "Wrap a Dafny-compiled involution into a codec map compatible with
   the transform registry. The function is used for both encode and decode."
  [id label dafny-fn]
  (let [f (fn [s] (<-dafny-string (dafny-fn (->dafny-string s))))]
    {:id          id
     :label       label
     :fn          f
     :involution? true
     :inverse?    true
     :verified?   true}))

(defn wrap-verified-bijection
  "Wrap a Dafny-compiled bijection (encode + decode pair)."
  [id label dafny-encode dafny-decode]
  {:id       id
   :label    label
   :encode   (fn [s] (<-dafny-string (dafny-encode (->dafny-string s))))
   :decode   (fn [s] (<-dafny-string (dafny-decode (->dafny-string s))))
   :inverse? true
   :verified? true})

;; Verified involutions (from spec/dafny/Involutions.dfy)
(def verified-identity  (wrap-verified-involution :v-identity  "identity*"  __default/Identity))
(def verified-reverse   (wrap-verified-involution :v-reverse   "reverse*"   __default/Reverse))
(def verified-rot13     (wrap-verified-involution :v-rot13     "rot13*"     __default/Rot13))
(def verified-jump5     (wrap-verified-involution :v-jump5     "jump5*"     __default/Jump5))
(def verified-atbash    (wrap-verified-involution :v-atbash    "atbash*"    __default/Atbash))

(def verified-codecs
  "Registry of verified codecs. These have Dafny proofs of their algebraic
   properties (involution, round-trip) compiled into the JAR."
  [verified-identity verified-reverse verified-rot13
   verified-jump5 verified-atbash])

The :verified? true flag distinguishes these from the unverified registry entries. A UI could display a green checkmark for verified codecs.

8. Implementation Plan

8.1. Phase 0: Install Dafny — DONE

Dafny 4.11.0 runs natively on FreeBSD 15.0. See Dafny on FreeBSD 15.0 for the full installation walkthrough and toolchain verification.

pkg install dotnet-sdk z3
dotnet tool install --global dafny
export PATH="$HOME/.dotnet/tools:$PATH"
dafny --version   # 4.11.0

8.2. Phase 1: Consolidate Dafny specs

Merge the local spec/dafny/Involutions.dfy (which has jump5 and atbash, absent from the external repo) with aygp-dr/transform-tool-dafny's Codec.dfy, Pipeline.dfy, Proofs.dfy, and CaesarProof.dfy.

Target structure:

spec/dafny/
  Codec.dfy          # algebraic model (from aygp-dr, extended)
  Involutions.dfy    # 5 involutions + proofs (local, all 5 codecs)
  Bijections.dfy     # encode/decode pairs (hex, base64, xor, etc.)
  Pipeline.dfy       # composition and reversal (from aygp-dr)
  Proofs.dfy         # collected theorems (from aygp-dr, extended)
  CaesarProof.dfy    # parameterized codec (from aygp-dr)

8.3. Phase 2: Compile to Java

dafny build --target:java spec/dafny/Involutions.dfy -o spec/dafny/out
cd spec/dafny/out && gradle jar

Add a deps.edn alias for the compiled JAR:

{:aliases
 {:dafny {:extra-paths ["spec/dafny/out/build/libs"]
          :extra-deps  {}}}}

8.4. Phase 3: Build the bridge

Create src/wal_sh/tools/transform/dafny_bridge.clj as described in 7.

8.5. Phase 4: Equivalence tests

test/wal_sh/tools/transform/dafny_equiv_test.clj runs both implementations side-by-side and asserts identical output for 1000 random ASCII strings per codec.

8.6. Phase 5: JavaScript target (parallel track)

dafny build --target:js emits CommonJS modules. The output uses a _dafny runtime with types like _dafny.Seq and _dafny.CodePoint.

Integration with shadow-cljs:

;; shadow-cljs.edn
{:builds
 {:transform
  {:js-options
   {:resolve {"dafny-runtime" {:target :npm-module
                                :require "spec/dafny/js-out/dafny/dafny.js"}}}}}}

Lower priority than the JVM path. The browser tool already works; the JS target adds verified browser codecs.

8.7. Phase 6: GraalVM native-image

With GraalVM 21.0.8 on this host:

native-image -jar spec/dafny/out/build/libs/Involutions.jar \
  --no-fallback -o verified-codecs

This produces a static binary with verified codecs. No JVM startup. Sub- millisecond invocation. Useful for CLI tools or CGI endpoints.

9. Mapping the Spec Gaps to the Clojure Implementation

The nine spec gaps identified by the external Dafny review correspond to concrete code in the Clojure implementation. Closing them requires changes to the spec, not the code.

Gap Clojure reality Resolution needed
SG-1 .cljc reader conditionals: JVM char vs JS .charCodeAt Specify: UTF-16 code units (JS) or Unicode scalars (--unicode-char)
SG-2 spec.cljc has domain map with malli schemas Formalize domain predicates in Dafny requires clauses
SG-3 :caesar defaults to 13, (first args) parsed Specify: empty args -> 13, N range [0,25], negative N rejected
SG-4 corrupt-1-bit uses java.util.Random / Math.random Exclude from purity claim; mark :deterministic? false
SG-5 rot13/rot13 preserves case (upper stays upper) Specify: case-preserving rot13 on ASCII letters
SG-6 resolve-fn returns nil for surjection decode Specify: nil -> {:error "no decode"} in apply-step
SG-7 :inverse? true is structural, not semantic Add :verified? true flag for Dafny-backed codecs
SG-8 f->c / c->f use IEEE 754 doubles via (Double/parseDouble) Specify: IEEE 754 with no epsilon; or switch to BigDecimal
SG-9 sort uses Clojure's (sort ...) which is Unicode codepoint order Specify: lexicographic Unicode codepoint order

SG-2 is the most actionable. The malli schemas in spec.cljc already define the domains executably:

(def domain
  {:identity   :string
   :reverse    :string
   :rot13      :string
   ;; ...
   :ipv4-int   [:and :string [:fn ipv4?]]})  ; {s // DottedQuad s}

Translating these to Dafny requires clauses is mechanical:

predicate IsIPv4(s: string) {
  exists a, b, c, d ::
    0 <= a <= 255 && 0 <= b <= 255 &&
    0 <= c <= 255 && 0 <= d <= 255 &&
    s == ToString(a) + "." + ToString(b) + "." +
         ToString(c) + "." + ToString(d)
}

method IPv4ToInt(s: string) returns (r: string)
  requires IsIPv4(s)
  ensures IntToIPv4(r) == s  // round-trip on domain

10. The Translation Gap

The core argument for Dafny-to-Java compilation is that it eliminates the translation gap between proof and runtime. Currently:

Lean 4:    Codec.roundtrip : ∀ a, decode (encode a) = a     [proved]
                      ↓
           (manual correspondence — human reviews both)
                      ↓
Clojure:   (= s (dec (enc s)))                                [tested]

The human must verify that the Lean encode and the Clojure :encode implement the same function. For rot13 this is trivial. For base64 with RFC 4648 padding rules, it is not.

With Dafny:

Dafny:     lemma Rot13Involutive(s) ensures Rot13(Rot13(s)) == s  [proved]
                      ↓
           dafny build --target:java
                      ↓
Java:      public static DafnySequence<Character> Rot13(...)       [compiled]
                      ↓
           Clojure interop
                      ↓
Clojure:   (verified-rot13 s)                                      [runs]

The Dafny Rot13 function IS the Java Rot13 method. The proof and the runtime are the same code path. No translation gap.

The trade-off: Dafny's verification is not as deep as Lean 4's dependent types. The Lean proof of encode_inj (injectivity forced by the Codec record) has no direct Dafny counterpart — Dafny proves properties about specific functions, while Lean proves properties about the type structure that forces all inhabitants to be correct.

This is the Curry-Howard correspondence in practice: in Lean 4, the Codec type is the proposition and any value of that type is a proof. In Dafny, the ensures clause is the proposition and the verifier discharges it. Both arrive at "program = proof" but via different mechanisms — Lean through the type system, Dafny through automated reasoning (Z3). The Dafny path compiles to Java; the Lean path does not (yet).

11. Open Questions

  1. DafnySequence performance: asString and verbatimString both copy. For a pipeline of N steps, the naive bridge performs 2N copies. The optimized path (keep DafnySequence throughout, convert once) reduces to 2. Is the constant factor acceptable for interactive use?
  2. FreeBSD .NET support: Cross-compilation is the pragmatic answer. Should we set up a CI job that compiles .dfy -> .jar on Linux and publishes the JAR as a Maven artifact?
  3. Verification depth: Lean vs. Dafny: Lean 4 proves Codec.roundtrip as a dependent record field — the type system prevents non-round- tripping codecs from existing. Dafny proves ensures decode(encode(x)) = x= as a postcondition on a specific function. The Lean approach is stronger (it covers all future codecs by construction), but the Dafny approach produces a runnable artifact. Both layers have value.
  4. GraalVM native-image: The Dafny Java runtime uses reflection for type descriptors. Does native-image handle this, or does it need a reflect-config.json?
  5. ClojureScript path: Dafny's JS target emits CommonJS. shadow-cljs can consume this via :npm-module deps. The glue is thin: wrap _dafny.Seq.asString and .verbatimString() the same way. Worth pursuing only after the JVM path is validated.
  6. assume false in the pipeline round-trip: The external repo's Pipeline.dfy has assume false in the core theorem. This is honest (the proof is incomplete), but it means the theorem is vacuously true. Can we close this by resolving SG-2 and SG-7?