Towards Multi-language Program Verification for AI

Raúl Raja Raúl Raja CTO at Predictable Machines

David Mazarro David Mazarro Founding Engineer at Predictable Machines

2026

Testing shows the presence of bugs, but never their absence

Headline!!

Edsger W. Dijkstra

How many test cases would it take?

Sometimes, testing could rule out bugs if you test all possible inputs, but:

Function

Possible inputs

Test all inputs?

and(Bool, Bool): Bool

2 \times 2 = 4

yes

multiply(Int32, Int32): Int32

2^{32} \times 2^{32} \approx 1.8 \times 10^{19}

impractical

toUpper(String): String

\infty

impossible

... so yeah, not really.

A sea of inputs

unit tests check individual inputs some buggy input a property covers some behavior but some bugs can still go unnoticed a complete specification covers all behavior

For any given program, you have a sea of possible inputs and expected behaviors

Testing doesn't cover all behavior

unit tests check individual inputs some buggy input a property covers some behavior but some bugs can still go unnoticed a complete specification covers all behavior

What is formal verification?

“Formal verification is a set of techniques used to prove the correctness of a program against a given formal specification”

Correctness: a program is correct if it behaves as expected

Prove: use mathematical techniquesthese are the famous formal methods! to guarantee this correctness

…

so what about formal specification?

What is a specification?

A specification is a description of a program's behavior.

class Account {
  // ...
  withdraw(amount: number) {
    this.balance -= amount;
    this.history.push({
      withdrawn: amount,
      balanceAfter: this.balance,
    });
    if (amount >= 1000) {
      this.flaggedForReview = true;
      sendEmail(this.owner, "Large withdrawal");
    }
    db.accounts.save(this);
  }
}

“If amount does not exceed acc.balance, acc.withdraw(amount) subtracts amount from the balance, records the withdrawal in acc.history and saves the account. If amount is 1000 or more, it sets acc.flaggedForReview and emails the account holder. The balance never goes negative.”

Formal specifications through Hoare logic

Hoare logic (C. A. R. Hoare, 1969) is a logical framework useful for describing what a program does from what must hold before and after it runs.

\{\,P\,\}precondition Cprogram \{\,Q\,\}postcondition

Hoare triple

If the precondition P holds before running the program C, the postcondition Q holds after.

Popping a stack

For a stack s, s.pop() throws an exception when s is empty.

Example

\hoare{\texttt{s.size()} = n,\ n > 0}{\texttt{s.pop()}}{\texttt{s.size()} = n - 1}

n is the size before the call.

Function contracts

class Account {
  // ...
  withdraw(amount: number) {
    this.balance -= amount;
    this.history.push({
      withdrawn: amount,
      balanceAfter: this.balance,
    });
    if (amount >= 1000) {
      this.flaggedForReview = true;
      sendEmail(this.owner, "Large withdrawal");
    }
    db.accounts.save(this);
  }
}

Partial withdraw contract

“If amount does not exceed acc.balance [...] the balance never goes negative.”

\begin{gathered} \{\,\texttt{amount} ≤ \texttt{acc.balance}\,\} \\ \texttt{acc.withdraw(amount)} \\ \{\,\texttt{acc.balance} ≥ 0\,\} \end{gathered}

Verification tools

A programming language defines a program and a specification language defines a specification; a verification tool checks whether the program matches the specification. If not, the program needs to be fixed; if so, it is correct.

Formal verification covers all behavior

unit tests check individual inputs some buggy input a property covers some behavior but some bugs can still go unnoticed a complete specification covers all behavior

How this translates to functional programming

Types and values

Bool true false String "λ" "Málaga" "" Int … −1000 −2 −1 0 1 2 3 42 1000 … 3 : Int { balance : Int // balance ≥ 0 } balances that never go negative

Logic as types: the Curry-Howard correspondence

Logic

Type (in Lean)

As a program

P \wedge Q

P × Q

a pair

P \vee Q

P ⊕ Q

one or the other

P \to Q

P → Q

a function

\top

Unit

always constructible

\bot

Empty

impossible to construct

\neg P

P → Empty

no way to build a P

proof of P

p : P

a program of type P

So the type system of your statically typed language is indeed a formal verification tool!

But it is missing something…

const acc = new Account(100);

acc.withdraw(30);   // balance: 70
acc.withdraw(60);   // balance: 10
acc.withdraw(50);

Type checker

Every call is well-typed

Specification

acc.balance ≥ 0 is violated: the balance is −40

Introduction to dependent types

Dependent types

inductive Vec (α : Type) : Nat → Type where | nil : Vec α 0 | cons : α → Vec α n → Vec α (n + 1) def Vec.head : Vec α (n + 1) → α | .cons x _ => x

The empty case is impossible, and Lean proves it for us.

Types are specifications

def Account.withdraw (acc : Account) (amount : Nat) -- precondition (h : amount ≤ acc.balance) : -- postcondition { new : Account // new.balance ≥ 0 } := let newBalance := acc.balance - amount let tx : Transaction := { withdrawn := amount, balanceAfter := newBalance } let new : Account := { balance := newBalance history := tx :: acc.history lowBalanceAlert := newBalance < 50 } ⟨new, by show newBalance ≥ 0; omega⟩

The precondition is an argument; the postcondition is the result's type.

Contracts as theorems

def withdraw (acc : Account) (amount : Nat) : Account := let newBalance := acc.balance - amount let tx : Transaction := { withdrawn := amount, balanceAfter := newBalance } { balance := newBalance history := tx :: acc.history lowBalanceAlert := newBalance < 50 } theorem withdraw_nonneg (acc : Account) (amount : Nat) -- precondition (h : amount ≤ acc.balance) : -- postcondition (withdraw acc amount).balance ≥ 0 := by grind [withdraw]

The function is plain code; its contract is a separate theorem.

Hoare triples in Lean

def withdraw (amount : Nat) : StateM Account Unit := do let acc ← get let newBalance := acc.balance - amount let tx : Transaction := { withdrawn := amount, balanceAfter := newBalance } let new : Account := { balance := newBalance history := tx :: acc.history lowBalanceAlert := newBalance < 50 } set new
theorem withdraw_spec (amount : Nat) : ⦃ fun (acc : Account) => amount ≤ acc.balance ⦄ withdraw amount ⦃ fun _ acc => acc.balance ≥ 0 ⦄ := by vcgen [withdraw] grind

Logic as types: the Curry-Howard correspondence

Logic

Type (in Lean)

As a program

P \wedge Q

P × Q

a pair

P \vee Q

P ⊕ Q

one or the other

P \to Q

P → Q

a function

\top

Unit

always constructible

\bot

Empty

impossible to construct

\neg P

P → Empty

no way to build a P

proof of P

p : P

a program of type P

\forall x : A,\ P(x)

∀ x : A, P x

the result type depends on the argument

\exists x : A,\ P(x)

∃ x : A, P x

a value, paired with evidence about it

Hoare triples and dependent types

Hoare triple

\{\texttt{amount} ≤ \texttt{acc.balance}\}\ \texttt{acc.withdraw(amount)}\ \{\texttt{acc.balance} ≥ 0\}

Dependent type

def withdraw (acc : Account) (amount : Nat) -- hypothesis (h : amount ≤ acc.balance) : -- return type { new : Account // new.balance ≥ 0 }

Theorem

def withdraw (acc : Account) (amount : Nat) : Account theorem withdraw_nonneg (acc : Account) (amount : Nat) -- hypothesis (h : amount ≤ acc.balance) : -- conclusion (withdraw acc amount).balance ≥ 0

The type checker as the verification tool

A dependently typed programming language holds both the specification, the type of append, which says the result's length is the sum of the inputs' lengths, and the program, its body; the type checker checks whether the program type checks. If not, the program needs to be fixed; if so, it is correct.

def append : List α n → List α m → List α (m + n) | .nil, ys => ys | .cons x xs, ys => .cons x (append xs ys)

A language with a dependent type system like Lean gives us a super powerful formal verification tool “for free”

Not only Lean

Lean is the hot thing in this field, but there are others:

Agda logo

Agda

Idris logo

Idris

Rocq logo

Rocq

“Solving” formal verification for different programming languages

One verification tool per language?

JavaKotlinTypeScriptC#JavaScriptNull safetyBounds & overflowContractsA tool for each language5 × 3 = 15 integrations A tool for a common language, and translate to it 5 + 3 = 8 integrations Verification tool, e.g. LeanIntermediate representationfunctions · branches · calls · effects · typesHigh-level languagesmany constructs, many semanticsLower levela common semantic groundOne verifierfor every language above IR JavaKotlinTypeScriptC#JavaScriptNull safetyBounds & overflowContracts

Why Lean

An expressive programming language and type system in the same package: huge flexibility to try different approaches to these problems

Ready-to-use building blocks, such as vcgen

Extensibility: teams can mold and shape its capabilities to their needs

def withdrawAll (amounts : List Nat) : StateM Account Unit := do for a in amounts do withdraw a theorem withdrawAll_spec (amounts : List Nat) : ⦃ fun (acc : Account) => amounts.sum ≤ acc.balance ⦄ withdrawAll amounts ⦃ fun _ acc => acc.balance ≥ 0 ⦄ := by vcgen [withdrawAll, withdraw] invariants · fun _ rest _ acc => rest.sum ≤ acc.balance with finish

Our current approach for Predictable Code

Source Compiler front end Common IR Engines Verdicts Java Kotlin TypeScript C# Spoon on Eclipse JDT K2 compiler plugin tsc Roslyn Predictable IL IL deciders structural facts Lean + SMT to discharge proof obligations Witness runs a test in the source language verified violated with a counterexample as a runnable test

We are still figuring out what works best for us and our partners

Typical challenges, and where AI changes the picture

With AI: faster development, but less reliable outputs

time code written checked AI assistants written checked the gap

A more pressing need to tackle formal verification challenges

Formal verification is still incredibly hard, even with AI

Requirement “The balance never goes negative.” LLM autoformalization Formal specification ∀ a, a.balance ≥ 0 Lean the kernel checks the proof checked by the kernel nothing checks that the specification says what we meant

Autoformalization does not solve much if you want to be hands-off: results cannot be trusted blindly.

Where it gets better: maintenance

Maintenance of projects is way easier now.

PROOFS The code changes A proof breaks AI proposes a fix The kernel re-checks it a wrong fix does not check LANGUAGE INTEGRATIONS A language changes Its integration breaks AI updates it The tests re-check it a wrong update fails the tests

Towards formal semantics

Formal semantics at the different layers of the pipeline allow for provably correct translations between software representations.

Source program translate IR translate Lean semantics semantics semantics ⟦ source ⟧ ⟦ IR ⟧ ⟦ Lean ⟧ ≈ ≈ proved equivalent proved equivalent

Semantics Done Quick

Semantics Done Quick, a project by Oath Technologies FRO:

We want to build semantic models fast, with AI doing most of the work, and trust the results in high-stakes settings.

  • “A desugaring function maps surface programs into core programs.”

  • “We validate against the reference implementation by randomised differential testing on a fuzzed corpus of programs.”

  • “Human expert attention is reserved for the hard design judgements where AI is unreliable.”

Mike Dodds, Oath Technologies, May 2026: oath.tech/pub/2026/05/semantics-done-quick

Granularity of specifications vs. verifiability

generality verifiability index in bounds no null dereference balance ≥ 0 withdrawals are recorded in order large withdrawals are flagged “the bank is fair” ? what we want

There is no reliable, hands-⁠off approach to turn natural language requirements into formally verifiable requirements

Automation Verifiability fully hands-off not possible

At Predictable Machines, we are navigating this spectrum to find practical verification solutions for our partners

Automation Verifiability fully hands-off not possible

Takeaways

  • Takeaway to be written

  • Takeaway to be written

  • Takeaway to be written

Questions?

Thank you!

2026