What changes when software properties become theorems?
Home · Docs · Type Theory · Proofs · Verified Software · ProofScript → Lean + TypeScript · Reference · Examples · Download
Software correctness is not one property. A real system can be type-correct but logically wrong, logically verified against the wrong specification, correctly compiled but misconfigured at runtime, or perfectly implemented while depending on a broken external service.
ProofScript is useful because it lets you make selected correctness claims precise, executable as proof obligations, and machine-checkable.
It does not make the rest of engineering disappear.
Most TypeScript projects already use several correctness tools:
static types
unit tests
property tests
runtime validation
linting
code review
integration tests
monitoring
Proof adds another kind of evidence:
A theorem can establish a formally stated property for every value covered by its quantified variables and assumptions.
That is different from testing selected executions.
The strongest practice combines them.
Think of assurance as layers rather than one “verified” badge.
1. Parses / elaborates
↓
2. Type-correct
↓
3. Property stated
↓
4. Property proved
↓
5. Assumptions inspected
↓
6. Source ↔ generated artifacts connected
↓
7. Runtime boundaries modeled / identified
↓
8. Deployment actually runs the intended artifact
Each layer rules out different failure modes.
A theorem is most valuable when you know exactly which layers it covers.
A familiar TypeScript API might accept:
function get<A>(xs: A[], i: number): A
The type does not encode that i lies inside this particular array.
A dependent interface can instead use:
i : Fin(xs.size)
Conceptually:
def get{A: Type}(xs: Array(A), i: Fin(xs.size)): A := {
xs[i]
}
“
getaccepts only an index carrying proof that it is smaller than the size ofxs.”
The bounds relationship is no longer a comment or caller convention. It appears in the checked interface.
Fin semantics unless correspondence is established;This pattern shows how dependent types can turn a runtime precondition into a compile/proof-time relationship.
Another common invariant:
0 < xs.size
A function can demand explicit evidence:
def first(xs: Array(Nat), h: 0 < xs.size): Nat := {
xs[0]
}
The parameter:
h : 0 < xs.size
is a proof.
This is more precise than accepting an unchecked array and hoping the caller remembered the precondition.
But note the subtlety: this says the function is correct under the supplied proof and source model. If an effectful/foreign environment can mutate xs between checking the proof and reading the array, that runtime behavior must be modeled or ruled out by the verified subset.
Many software systems normalize data:
A desirable property is idempotence:
normalize(normalize(x)) = normalize(x)
A theorem might be:
theorem normalizeIdempotent(x: Config):
normalize(normalize(x)) = normalize(x) := by {
...
}
If normalization is idempotent, repeatedly running it does not keep changing already normalized data.
This can support reasoning about caches, serialization, configuration loading, and convergence.
It does not prove normalization preserves every piece of information you care about.
You may also need a semantic-preservation theorem such as:
Meaning(normalize(x)) = Meaning(x)
The lesson is reusable:
A mathematically elegant property is not automatically a complete software specification.
Suppose:
encode : Message → Bytes
decode : Bytes → Option(Message)
A strong correctness property is:
decode(encode(x)) = some(x)
Read:
“If we encode any valid message and immediately decode the bytes, we recover exactly that message.”
This theorem covers all values x in the modeled Message domain.
But it does not establish:
encode/decode semantics match the protocol specification.Additional theorems can address those separately.
A second property might be:
encode(x) = encode(y) → x = y
for a serialization intended to be injective.
Or canonicality may be expressed through normalization of byte forms.
Combining properties gives a stronger story:
round trip
+
canonicality / injectivity
+
malformed-input handling
+
resource bounds
Formal verification encourages you to decompose “serializer is correct” into exact claims.
A sorting algorithm should usually satisfy at least two core properties.
Ordered(sort(xs))
Permutation(xs, sort(xs))
Why both?
Because:
sort(xs) := []
is trivially ordered.
And returning the input preserves all elements but may not be ordered.
Together, the theorems approximate the ordinary mathematical specification of sorting.
You may also care about:
stability
complexity
comparator consistency
Those are separate properties.
For applications where equal-key elements must retain original order, ordinary permutation preservation is not enough.
You need a property referring to original relative positions for equal keys.
This is a good example of why formal verification changes design conversations:
“Correct sorting” is not one theorem until you define what correct means for your application.
ProofScript cannot choose that policy for you. It can require evidence once you choose it.
Suppose a runtime decision function returns an authorization result:
authorize(user, action)
A soundness theorem might say:
authorize(user, action) = allowed
→
PolicyPermits(user, action)
Read:
“Whenever the executable authorization function says
allowed, the formal policy also permits the action.”
This is valuable because it connects executable decision logic to a declarative policy.
But soundness alone may be too conservative: a function that always denies access satisfies the theorem.
You may also want a completeness theorem:
PolicyPermits(user, action)
→
authorize(user, action) = allowed
Together they establish closer correspondence between the implementation and policy.
A theorem about authorization does not automatically prove “the system is secure.”
Questions still include:
authorize?Formal proof is strongest when paired with an explicit threat model and clear runtime boundary assumptions.
Many business workflows are state machines:
Pending → Authorized → Captured → Refunded
You may want to prove an invariant such as:
Captured(state) → PreviouslyAuthorized(state)
Or in transition-preservation form:
Invariant(s)
→
ValidTransition(s, s')
→
Invariant(s')
This theorem says:
“If the invariant holds before a valid transition, it also holds afterward.”
Combined with an initial-state proof, induction over transition sequences can establish the invariant for all reachable modeled states.
This is a powerful fit for formal methods because many application bugs are illegal-state-transition bugs.
Consider a modeled account balance.
A property might be:
amount ≤ balance
→
withdraw(balance, amount) ≤ balance
or stronger:
withdraw(balance, amount) = balance - amount
for Nat balances.
But real financial software needs much more precision:
The theorem is only about the model actually written.
A good formal system makes that limitation visible rather than hiding it behind the word “verified.”
For iterative algorithms, a loop invariant states a property that must remain true across iterations.
Conceptually:
initially: invariant holds
step: invariant + loop body → invariant still holds
exit: invariant + exit condition → desired postcondition
ProofScript’s verification vocabulary includes invariant for modeled loops where a sound lowering/proof-obligation framework exists.
An invariant is not a runtime assertion by default. It is a proof obligation in the verification model.
requires and ensuresA contract can make preconditions and postconditions explicit.
def pred(n: Nat): Nat
contract value {
requires hPositive: 0 < n;
ensures (result) => result < n := by {
omega
};
}
:= {
n - 1
}
requiresRead:
“The verified guarantee assumes this proposition.”
ensuresRead:
“The returned value must satisfy this proposition.”
resultA name for the returned value inside the postcondition.
A failed postcondition proof is not repaired by silently inserting an assumption. The verification layer must produce ordinary valid proof obligations under the same logical foundation.
Suppose:
requires n < 0
for n : Nat.
That precondition is impossible.
A postcondition can then be logically true under the impossible assumption while giving no useful behavior for real callers.
So high-assurance specifications may also prove domain/satisfiability facts:
∃ input, Precondition(input)
or prove that real construction paths can establish the precondition.
Again: validity and usefulness are related but distinct.
old(...) and pre-state reasoningFor stateful specifications you often need to compare the result with the state at function entry.
Conceptually:
old(balance)
means:
“The value of
balancein the formally defined entry/pre-state.”
This is not a magical snapshot of arbitrary JavaScript memory.
A sound meaning requires a state model that says what the pre-state is, which values can be referenced, and how effects relate entry and exit states.
Without that model, old is just suggestive syntax, not a guarantee.
Verification sometimes needs information used only for specification/proof.
That is the role of ghost data.
Examples:
If ghost data is erased from runtime execution, a sound system needs a non-interference discipline: runtime-relevant behavior must not secretly depend on erased proof-only information in an invalid way.
Proof erasure is a theorem/design property, not merely deleting fields from generated TypeScript.
For a pure total function:
f : A → B
an execution-correspondence claim can often be stated relatively cleanly:
for corresponding input a,
source evaluation f(a)
corresponds to
target evaluation f_ts(a)
The meaning of “corresponds” still needs representations and observations, but there are fewer external effects to account for.
This is why verified subsets often begin with pure computation.
Once software interacts with:
“same behavior” requires a more detailed observation model.
For example, should two programs count as equivalent if they return the same result but perform network requests in a different order?
There is no universal answer. The semantic specification must say which observations matter.
A theorem over:
Nat
uses natural-number semantics.
If a TypeScript emitter compiled that to ordinary JS number without preserving large integer behavior, the theorem would not automatically apply to large runtime values.
A correct backend must either:
Silently changing mathematics to match JavaScript convenience would break the meaning of the source program.
A compiler does not need to translate every valid ProofScript construct into every executable form.
For a verified TypeScript subset, this can be the correct behavior:
valid ProofScript program
+
construct not faithfully supported by verified TS profile
↓
explicit unsupported / refusal
That is better than generating code whose semantics only approximately resemble the source.
Verified compiler work such as CompCert makes the same broad point: successful compilation is conditional, and the compiler is allowed to reject rather than emit semantically untrusted output.
A transfer function can accidentally debit one account without crediting the other, double-credit after a retry, or apply a fee incorrectly.
A pure model might look like:
function transfer(state: Ledger, from: AccountId, to: AccountId, amount: Nat): Ledger := {
...
}
A central theorem could state:
TotalBalance(transfer(state, from, to, amount))
=
TotalBalance(state)
“The modeled transfer moves value between accounts but does not create or destroy total value.”
Accounting systems often have many branches: insufficient funds, fees, reversals, refunds, and retries. A conservation theorem constrains every branch covered by the formal definition.
It does not automatically establish database transaction isolation, that an external payment processor behaves correctly, or that TotalBalance matches your legal/accounting definition. Those are separate model and environment questions.
Imagine an inventory reducer:
function applyInventory(state: Inventory, event: InventoryEvent): Inventory := {
...
}
A property can say:
ValidInventory(state)
→
Accepted(event, state)
→
ValidInventory(applyInventory(state, event))
where ValidInventory includes the invariant that every stock quantity is nonnegative.
An inventory bug may occur only after a rare sequence such as:
reserve
→ cancel
→ retry shipment
→ process late return
A state invariant is about all modeled legal transitions, not merely sequences anticipated by test authors.
Distributed systems retry operations because clients cannot always distinguish “the server failed” from “the server succeeded but the response was lost.”
For an operation intended to be idempotent:
function applyRequest(req: Request, state: State): State := {
...
}
state:
applyRequest(req, applyRequest(req, state))
=
applyRequest(req, state)
“Processing the same request twice has the same modeled state result as processing it once.”
This can be crucial for payment initiation, job scheduling, webhook handling, provisioning, and message consumers.
It still does not prove that an external side effect was executed exactly once unless that side effect is part of the formal model/correspondence boundary.
A function can have dozens of policy branches:
function authorize(user: User, resource: Resource, action: Action): Decision := {
...
}
A soundness theorem:
authorize(user, resource, action) = .allowed
→
PolicyPermits(user, resource, action)
rules out a modeled execution where the implementation says allowed but the formal policy says forbidden.
For stronger correspondence, also consider completeness:
PolicyPermits(user, resource, action)
→
authorize(user, resource, action) = .allowed
The two theorems answer different questions:
soundness → no unauthorized allow
completeness → no policy-approved request is incorrectly denied
Parsers often stand between hostile/untrusted input and trusted internal code.
function parseConfig(text: String): Option(Config) := {
...
}
Useful theorem:
parseConfig(text) = .some(config)
→
ValidConfig(config)
A separate round-trip theorem may say:
parseConfig(renderConfig(config)) = .some(config)
These prove different things: validity of successful parsing versus round-trip behavior for rendered valid values.
Suppose an application migrates data:
function migrate(old: SchemaV1): SchemaV2 := {
...
}
Possible theorem:
ValidV1(old)
→
ValidV2(migrate(old))
If there is a compatibility projection:
projectV1(migrate(old)) = old
may express a stronger round-trip-style property.
This is valuable when migrations touch every stored record: the cost of one semantic mistake can be much larger than a localized UI bug.
Consider an order lifecycle:
Pending → Authorized → Captured → Shipped
↘ Refunded
Pending → Cancelled
A transition function can carry an invariant such as:
Shipped(order) → PreviouslyCaptured(order)
or, more directly, prove preservation of a state predicate across every accepted transition.
The value of proof is not that state machines are complicated mathematics. It is that rare event orderings cause real bugs, and induction over transition sequences can reason beyond a finite set of scripted scenarios.
Security-sensitive software frequently normalizes identifiers, paths, headers, URLs, or policy input. If two subsystems normalize differently, validation may inspect one representation while execution uses another.
A fundamental law is idempotence:
normalize(normalize(x)) = normalize(x)
Another is preservation of meaning:
Meaning(normalize(x)) = Meaning(x)
when the intended normalization is semantics-preserving.
The first theorem alone does not imply the second. Again, precise theorem choice matters.
The phrase should be used carefully.
A defensible strong claim is:
“This generated TypeScript artifact is connected to the checked ProofScript source by the declared execution-correspondence evidence for this supported subset and toolchain.”
That is much better than:
“Lean proved the JavaScript.”
The first statement names the bridge. The second hides it.
Even a highly verified pure core can be surrounded by unverified integration code.
Tests are still valuable for:
Formal proof lets you spend testing effort differently, not eliminate empirical engineering.
With theorem-backed code, review can focus on new questions:
Formal proof can move review from “does this branch look right?” toward “is this the right mathematical contract?”
Not every line deserves a theorem.
High-return targets include:
A practical project can mix ordinary programming and deep proof rather than insisting on maximal proof density everywhere.
For any “verified” declaration, report:
A single green badge without these details is weak communication.
Think of correctness as a chain:
informal requirement
↓ specification design
formal proposition
↓ theorem proof
checked logical result
↓ source / artifact binding
generated Lean / TypeScript
↓ semantic correspondence
runtime artifact
↓ environment assumptions
real execution
ProofScript’s purpose is to make the middle of this chain dramatically stronger and more explicit, while refusing to pretend that the other links do not exist.