Statements
HeyVL statements describe programs and proofs inside procedure bodies.
Program statements
Proofs and semantics
Concrete Statementsβ
Blocksβ
A block is a sequence of statements enclosed by curly braces.
{ ... }
Variables declared inside a block are local to it. Assignments to outer variables remain visible afterwards:
var x: UInt = 1
{
var y: UInt = 2
x = x + y
}
// x is now 3
// y is no longer in scope
Blocks can be nested, and an empty block {} has no effect.
Semicolons between statements are optional.
Variable Declarationsβ
Declare a variable with its type and an optional initial value.
var x: T
var x: T = e
var count: UInt = 0 declares an unsigned integer with initial value 0.
Without an initializer, verification considers every possible value of the variable's type.
See the standard library for built-in types and domains for user-defined types.
Assignments and Procedure Callsβ
An assignment stores the value of an expression in a variable.
x = e
x = 39 + y updates x using the current value of y.
Assignments can update local variables and output parameters.
Input parameters are immutable.
The assigned value must have a compatible type.
A distribution call samples a random value. HeyVL provides Bernoulli, uniform, binomial, and hypergeometric distributions.
x = flip(p)
x = ber(pa, pb)
x = unif(a, b)
x = binom(n, pa, pb)
x = hyper(N, k, n)
var b: Bool = flip(0.5) assigns true or false with equal probability.
Distribution calls must appear directly on the right-hand side of an assignment or initializer.
flip accepts symbolic arguments.
The other distributions require numeric literals.
Arbitrary finite discrete probabilistic choices can be implemented using only fair binary probabilistic choice (flip(0.5)) and iteration (while loops)1.
You can also use [weigh] statements and the additive choice statement (if +) to implement any finite discrete distribution, as shown in the example below.
A procedure call assigns its outputs to a list of variables.
x, y = p(args)
p(args)
A call with no outputs stands alone. Caesar verifies calls against the callee's specification.
Boolean Choicesβ
An if executes one of two blocks according to a Boolean condition.
if b { ... } else { ... }
This decrements x if it is positive:
if x > 0 {
x = x - 1
} else {}
The else block is required, but may be empty.
While Loopsβ
HeyVL supports while loops that run a block of code while a condition evaluates to true.
while b { ... }
For example, keep flipping a coin until it returns false:
var cont: Bool = true
@invariant(...)
while cont {
cont = flip(0.5)
}
For verification, while loops need proof-rule annotations such as the @invariant(...) annotation in the example.
If a while loop does not have a proof-rule annotation, Caesar cannot verify the program and will show an error.
Proof-rule annotations also determine which fixpoint semantics to use for the loop. This can be least fixpoint, or bounded or unbounded greatest fixpoint. The fixpoint semantics determines which reward is collected for executions which do not terminate. Use calculus annotations on procedures to make this choice explicit.
With the model-checking translation, proof-rule annotations are not necessary. It supports probabilistic model checkers such as Storm for a subset of HeyVL programs. You can use these to get an initial estimate of expected values.
Rewardsβ
reward adds a reward, such as elapsed time or resource consumption.
reward e
The reward e must be non-negative.
tick e is an alias.
Rewards accumulate along an execution.
With post 0, reward 2; reward 3 has total reward .
A nonzero post adds a final reward.
Weightsβ
weigh multiplies the remaining program's contribution by a weight.
weigh e
The weight e must be non-negative.
For example, weigh 0.5; reward 2 contributes with post 0, while reward 2; weigh 0.5 contributes .
Combine weigh with additive choice if + to weight each branch and sum the results.
The example below uses this to express a probabilistic choice.
Nondeterministic Choicesβ
Demonic choice if β minimizes the expected outcome.
Angelic choice if β maximizes it.
Additive choice if + sums the contributions of both branches.
if β { ... } else { ... }
if β { ... } else { ... }
if + { ... } else { ... }
For example, choose between two rewards:
if β {
reward 2
} else {
reward 6
}
With post 0, changing the choice operator gives:
if β, the smaller reward.
if β, the larger reward.
if +, the sum of both rewards.
For ASCII spellings, use \cap for β, \cup for β, and \oplus for +.
Additive choice also accepts β.
One can use the additive choice operator and the weigh statement to implement any sampling from finite discrete distributions.
The example below applies this to implement a x = flip(0.3) assignment.
Probabilistic choice implemented with additive choice and weights
The example below implements a x = flip(0.3) assignment using additive choice and weights.
We just need to weigh the branches of the additive choice according to the desired probabilities.
proc expected_reward() -> (x: Bool)
pre 0.3
post [x]
{
if + {
weigh 0.3
x = true
} else {
weigh 0.7
x = false
}
}
This approach can be easily generalized to implement any finite discrete distribution.
Verification Statementsβ
Verification statements express conditions to prove, assumptions to use, and changes to the proof state.
Each verification statement has a dual with a co prefix, such as assert and coassert.
Both forms can occur in a proc or coproc.
These statements have quantitative semantics. We introduce common Boolean uses first for intuition, then give the general quantitative rules.
Caesar's proof rules generate these statements for supported proof techniques.
Verification Conditionsβ
An expectation assigns a non-negative number or to each program state.
Starting from post, Caesar works backwards through the body to compute a verification pre-expectation.
We write for the result before statement , where is the expectation for the remaining code.
All operations on expectations below are pointwise.
For a whole procedure body , the verification condition (VC) compares this result with pre in every initial state:
proc(lower bound).
coproc(upper bound).
A smaller pre-expectation can make a lower-bound proof harder and an upper-bound proof easier. Probabilistic failures can involve several statements across multiple paths. See the slicing guide for how to diagnose them.
The VS Code extension can show pre-expectations inline with the command Caesar: Explain Verification Condition Generation.
Rules for program statements
Sequential composition works backwards:
An empty block leaves unchanged. For a choice with branches , write . Each line gives for the indicated statement.
x = ex = flip(p), for .
if bif βif βif +reward eweigh e, with .
Assertionsβ
Add a condition that the proof must establish at this point.
The usual forms are assert in a proc and coassert in a coproc.
assert ?(b)
coassert !?(b)
For example, assert ?(x >= 1) adds the obligation that x is at least .
In a coproc, write coassert !?(x >= 1) for the same Boolean condition.
Assertions also accept a quantitative expectation e.
assert chooses demonically whether to stop and collect e or continue with the remaining program.
coassert makes this choice angelically.
assert e
coassert e
e has type EUReal.
The Boolean forms above use these same rules, with the embeddings ?(b) or !?(b) as e.
?(b) is when b holds and otherwise.
!?(b) is when b holds and otherwise.
When b holds, both forms leave unchanged.
Otherwise, assert ?(b) gives and coassert !?(b) gives , matching the bound direction of the proof.
The Iverson bracket [b] takes values and instead, for reasoning about probabilities.
Assumptionsβ
Assume a condition holds at this point in the proof.
The usual forms are assume in a proc and coassume in a coproc.
assume ?(b)
coassume !?(b)
For example, with x: UInt, this fragment uses x > 0 to prove x >= 1:
assume ?(x > 0)
assert ?(x >= 1)
In a coproc, use coassume !?(x > 0) followed by coassert !?(x >= 1).
Caesar uses the assumed condition without proving it.
Proof encodings use assumptions to record branch conditions, loop invariants, and procedure specifications.
Assumptions also accept a quantitative expectation e.
If the remaining expectation is at least e, assume makes a lower-bound goal trivial.
coassume does the same for an upper-bound goal when the expectation is at most e.
assume e
coassume e
The Boolean forms use these same rules with the embeddings ?(b) and !?(b).
When b holds, both leave unchanged.
Otherwise, assume ?(b) gives and coassume !?(b) gives .
Proving an assertion from an assumption
proc positive_integer(x: UInt) -> ()
pre \infty
post \infty
{
// \infty
assume ?(x > 0)
// ?(x >= 1)
assert ?(x >= 1)
}
The comments show the pre-expectation immediately before each statement.
The assertion requires x >= 1, which follows from the assumption x > 0.
The resulting pre-expectation is in every initial state, so the procedure verifies.
The dual proof uses to represent a satisfied obligation:
coproc positive_integer_dual(x: UInt) -> ()
pre 0
post 0
{
// 0
coassume !?(x > 0)
// !?(x >= 1)
coassert !?(x >= 1)
}
Removing the assumption makes either proof fail at x = 0.
Ending a proof branch
For an expectation I, this fragment has verification pre-expectation exactly , whatever follows it:
// I
assert I
// \infty
assume 0
Reading backwards, assume 0 transforms any continuation into , and assert I then yields .
Proof-rule encodings use this to finish a branch after introducing its obligation.
The dual fragment is:
// I
coassert I
// 0
coassume \infty
coassume makes the continuation , and coassert I gives .
For complete encodings, see procedure calls and Park induction.
Havocβ
Forget the current values of variables and choose replacements.
The choice is demonic for havoc and angelic for cohavoc.
havoc x, ...
cohavoc x, ...
To prove a Boolean property for every replacement value, the usual pattern uses havoc in a proc or cohavoc in a coproc.
For example, after forgetting the value of a local x: UInt, assume it is positive:
havoc x
assume ?(x > 0)
assert ?(x >= 1)
The proof now concerns an arbitrary positive integer, regardless of the earlier value of x.
Loop proofs use this pattern to forget values from earlier iterations and then assume the loop invariant.
Procedure-call encodings use it to forget the previous values of output variables.
Both statements accept local variables and output parameters.
Validationsβ
Turn the remaining expectation into a Boolean proof obligation.
validate
covalidate
Validation leaves Boolean obligations unchanged: values and stay as they are.
validate; assume 2 checks whether the expectation of the following code is at least .
This example fails because its post 1 is below that bound:
proc require_two() -> ()
pre 1
post 1
{
// 0
validate
// 1
assume 2
}
Without validate, the computed pre-expectation would be , so pre 1 would verify.
validate turns that value into , making the check fail.
With post 3, assume 2 produces , which validate preserves, so the procedure verifies.
validate; assume I returns .
Its dual covalidate; coassume I returns .
Combining assert, havoc, and assume
This procedure combines all four statements to verify pre 1:
proc check_bound() -> (x: UInt)
pre 1
post x + 2
{
// 1
assert 1
// \infty
havoc x
// \infty
validate
// \infty
assume x + 2
}
Reading backwards, assume x + 2 compares with the post, and validate turns the result into or .
havoc x checks the comparison for every replacement value of x, giving here.
assert 1 then gives .
With post x + 1, the comparison fails for every x.
Validation then gives , so the proof fails.
Without validate, the failed comparisons would leave .
havoc would give , so the procedure would still verify.
In general, assert P; havoc x; validate; assume Q gives if holds for every replacement value of x, and otherwise.
Replacing all four statements with their co forms gives if holds for every replacement value, and otherwise.
In both cases, is evaluated before forgetting x.
This pattern appears in procedure-call encodings and loop-invariant proofs.
Game Semanticsβ
HeyVL programs describe a game between two players: the demonic player minimizes expected reward, and the angelic player maximizes it. For the core HeyVL language, the game's value is the pre-expectation defined above.
Programs and choices.
A play follows assignments and sampling, collects reward values, and collects post on reaching the end.
The demonic player controls if β and havoc, the angelic player if β and cohavoc.
The remaining game value is the expected reward still to come, accounting for future random outcomes and both players' choices.
Assertions.
At assert e, the demonic player may stop for reward e or continue.
coassert e gives this choice to the angelic player.
Stopping keeps earlier rewards and does not collect future rewards, including post.2
Assumptions.
A referee evaluates the value of the remaining game.
For assume e, it ends play with reward when that value is at least e.
For coassume e, it ends play with reward when the value is at most e.
Otherwise, play continues.
Validations.
At validate, the referee lets play continue if the remaining value is , and otherwise stops it with reward .
At covalidate, it lets play continue if the remaining value is , and otherwise stops it with reward .
Deprecated Statementsβ
The legacy compare, cocompare, negate, and conegate statements may be removed from Caesar.
Prefer the supported proof rules and procedure-call syntax when encoding proofs.
Compareβ
compare f is shorthand for validate; assume f.
Its dual cocompare f is shorthand for covalidate; coassume f.
Negationsβ
The negate and conegate statements apply HeyLo's negations.
validate is equivalent to conegate; conegate, and covalidate is equivalent to negate; negate.
Sound (co)procedure calls require monotonicity. Negation statements can break this property and make calls unsound.
Further Readingβ
- OOPSLA '23: formal statement semantics and how
assert,havoc,validate, andassumeencode procedure calls. - CAV '26 (PDF): how HeyVL proof encodings connect to Caesar's verification backends and editor support.
- Weighted programming: how
weighand additive choice express weighted programs beyond probabilities. - Game semantics (AISoLA '24): interpreting verification statements as a game between two players, with a referee for assumptions and validations.
- Slicing (ESOP '26): why a failed proof can involve several statements and paths, and how slicing finds them.