Dafny–Clojure JVM Interop: Verified Transforms on the JVM
Table of Contents
- 1. Overview
- 2. Environment
- 3. Existing Dafny Verification
- 4. Dafny Compilation to Java
- 5. Interop Architecture
- 6. Type Mapping: Dafny to Clojure
- 7. The Clojure Bridge Namespace
- 8. Implementation Plan
- 9. Mapping the Spec Gaps to the Clojure Implementation
- 10. The Translation Gap
- 11. Open Questions
- 12. Related Work
;; 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 onCodecEntryto select the correct function for a given directionExecute: recursive pipeline execution with halt-on-first-error semanticsPipelineIsReversible: every step's codec is reversible and both directions resolveReversePipelineCorrect: reverses step order and flips directions, with involutions keepingEncode(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:
- 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. - 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.FixedPointCharacterizationprovesEncodeSameAsDecode(n) <==> (n =0 || n= 13). The spec's claim of "uniqueness" is imprecise about N=0. - Round-trip:
CaesarDecode(CaesarEncode(x, N), N) =x= for all x and all N in [0,25]. The proof reduces to the modular arithmetic lemmaShiftAndUnshift(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 generatedfunction: compiled to a static Java method (Dafny 4.x; wasfunction methodin 3.x)lemma: erased — the proof obligation is discharged at verify timemethod: compiled to a Java methodpredicate: erased if ghost, compiled if used in runtime coderequires/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
- DafnySequence performance:
asStringandverbatimStringboth copy. For a pipeline of N steps, the naive bridge performs 2N copies. The optimized path (keepDafnySequencethroughout, convert once) reduces to 2. Is the constant factor acceptable for interactive use? - FreeBSD .NET support: Cross-compilation is the pragmatic answer. Should
we set up a CI job that compiles
.dfy->.jaron Linux and publishes the JAR as a Maven artifact? - Verification depth: Lean vs. Dafny: Lean 4 proves
Codec.roundtripas a dependent record field — the type system prevents non-round- tripping codecs from existing. Dafny provesensures 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. - GraalVM native-image: The Dafny Java runtime uses reflection for type
descriptors. Does
native-imagehandle this, or does it need areflect-config.json? - ClojureScript path: Dafny's JS target emits CommonJS. shadow-cljs can
consume this via
:npm-moduledeps. The glue is thin: wrap_dafny.Seq.asStringand.verbatimString()the same way. Worth pursuing only after the JVM path is validated. assume falsein the pipeline round-trip: The external repo'sPipeline.dfyhasassume falsein 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?