Sound Gradual Verification with Symbolic Execution

Gradual verification, which supports explicitly partial specifications and verifies them with a combination of static and dynamic checks, makes verification more incremental and provides earlier feedback to developers. While an abstract, weakest precondition-based approach to gradual verification was previously proven sound, the approach did not provide sufficient guidance for implementation and optimization of the required run-time checks. More recently, gradual verification was implemented using symbolic execution techniques, but the soundness of the approach (as with related static checkers based on implicit dynamic frames) was an open question. This paper puts practical gradual verification on a sound footing with a formalization of symbolic execution, optimized run-time check generation, and run time execution. We prove our approach is sound; our proof also covers a core subset of the Viper tool, for which we are aware of no previous soundness result. Our formalization enabled us to find a soundness bug in an implemented gradual verification tool and describe the fix necessary to make it sound.


INTRODUCTION
Static veri cation technology based on Hoare-logic-styled pre-and postconditions [Hoare 1969] has come a long way in the last few decades.Such tools can now support the modular veri cation of data structures that manipulate the heap [Reynolds 2002;Smans et al. 2012] and are recursive [Parkinson and Bierman 2005].However, veri cation is expensive, requiring many auxiliary speci cations such as loop invariants and lemmas, and often costing an order of magnitude more human e ort than development alone.In response, Bader et al. [2018] introduced the idea of gradual veri cation, which supports the incremental speci cation and veri cation of code by seamlessly combining static and dynamic veri cation.A developer can now write partial, imprecise speci cations-formulas such as ?* .== 2-backed by run-time checking.During static veri cation, imprecise speci cations are strengthened in support of proof goals when it is necessary and non-contradictory to do so.Then, corresponding dynamic checks are inserted to ensure soundness.As a result, gradual veri cation allows users to specify and verify only the properties and components of their system that they care about, and incrementally increase the scope of veri cation as necessary.
Based on this early idea, Wise et al. [2020] and DiVincenzo et al. [2022] extended gradual verication to support recursive heap data structures.Wise et al. [2020] presented the rst theory of gradual veri cation for implicit dynamic frames (IDF) [Smans et al. 2012], a variant of separation logic [Reynolds 2002], and abstract predicates [Parkinson and Bierman 2005].Their design, corresponding theory, and proofs rely heavily on the backward-reasoning technique called weakest liberal preconditions (WLP), and on in nite sets that are not easy to approximate in nite form.Additionally, Wise et al. [2020]'s design checks all proof obligations at run-time, even when some obligations have been discharged statically.Therefore, it remained unclear how to implement gradual veri cation and whether gradually-veri ed programs could achieve good performance.Fortunately, in follow-up work, DiVincenzo et al. [2022] implemented and empirically evaluated Gradual C0, the rst gradual veri er that can be used on real programs.Gradual C0 is based on symbolic execution, a forward-reasoning technique which is routinely used in static veri ers such as Viper [Müller et al. 2016], and optimizes run-time checks with statically available information to improve run-time performance.DiVincenzo et al. [2022] showed that this improvement over prior work yields signi cant performance boosts.
Technically, Gradual C0 is built on top of Viper [Müller et al. 2016], which is a static veri cation infrastructure and tool that facilitates the development of program veri ers supporting IDF and recursive abstract predicates.Viper also uses symbolic execution at its core.Besides Gradual C0, an array of widely-used veri ers have been built on top of Viper, including Prusti [Astrauskas et al. 2022] for Rust, Nagini [Eilers and Müller 2018] for Python, and VerCors [Blom et al. 2017] for Java.However, despite its prominence, Viper has not been proven sound; nor have, to our knowledge, other symbolic execution-based methods for verifying IDF logics.Thanks to the complexities of symbolic execution and Viper's support for practical but advanced veri cation features, Schwerho [2016]'s speci cation of Viper is full of implementation details that make it di cult to formally state and prove soundness.Since Gradual C0 is built on Viper, this problem carries over to Gradual C0's speci cation in DiVincenzo et al. [2022] and is made worse by the combination of static and dynamic checking.Thus DiVincenzo et al. [2022] does not contain a proof of soundness for Gradual C0.Furthermore, since Gradual C0 uses symbolic execution instead of WLP and optimizes run-time checks, Wise et al. [2020]'s proof is also not applicable.This is problematic, because the intricate interactions of static and dynamic checking in gradual veri cation can easily lead to subtle soundness bugs in gradual veri ers like Gradual C0, as we will show in §8.
Therefore, this paper presents a formal statement and proof of soundness for Gradual C0 and its underlying core subset of Viper.We formalize Gradual C0's symbolic execution algorithm in sets of inference rules, rather than the CPS-style speci cation in DiVincenzo et al. [2022] and Schwerho [2016], to enable abstractions that improve the readability of the design and make it easier to state and prove soundness.The level of abstraction we use is far closer to the implementation of Gradual C0 than Wise et al. [2020]'s formal system, but slightly more abstract than DiVincenzo et al. [2022]'s CPS-style speci cation, which is littered with implementation details.Reaching the right level of abstraction for our goals took some trial and error.We re ect on this process, including our missteps, in this paper as well.Our approach is inspired by the formal system for a basic type checker combined with symbolic execution in Khoo et al. [2010].However, we separate the rules into several types of judgements to re ect the architecture of DiVincenzo et al. [2022] and Schwerho [2016] and deal with the complexities of IDF and gradual veri cation.Given an initial symbolic state, the rules compute a next possible state (of which many may exist), and a set of run-time checks required for this transition when optimism is relied upon.That is, our rules are non-deterministic, but only in regards to the multiple execution paths explored by symbolic execution at program points like if statements, while loops, and logical conditionals.Furthermore, we clearly separate the cases required to support imprecise speci cations from those dealing with the underlying veri cation algorithm supporting only complete static speci cations.Therefore, our formal system is a conservative extension of a core calculus of Viper; and so, by formalizing Gradual C0 and proving it sound, we have also formalized the core of Viper and proved it sound.To make it easier for readers of this paper to take advantage of our formal statement and proof of soundness for Viper for their own uses, we present rst a core language, which we call SVL C0 , along with veri cation rules modeling Gradual C0's underlying static veri cation algorithm.We then de ne GVL C0 , which extends SVL C0 to include gradual speci cations and corresponds to the full language used by Gradual C0.We also formally de ne static veri cation for GVL C0 , modeling the veri cation algorithm of Gradual C0.We hope this separation provides a solid foundation for future proof endeavors of other static veri ers based on symbolic execution.
In order to fully de ne the behavior of GVL C0 and its subset SVL C0 , we specify its dynamic semantics, which combines the semantics of C 0 [Arnold 2010] with the dynamic semantics of GVL RP , the language used to de ne the theory of gradual veri cation with recursive predicates in Wise et al. [2020].The C 0 programming language is a core, safe variant of the C language introduced for education [Arnold 2010] and is also supported by Gradual C0.C 0 allows speci cation of the preand post-conditions of methods, but does not include constructs necessary for static veri cation using IDF.Thus we add the dynamic semantics from Wise et al. [2020] for IDF speci cations, recursive predicates, and imprecise speci cations.These semantics assert the validity of every speci cation at run-time, ensuring both memory safety and functional correctness of programs.Thus these semantics provide a foundation against which we can establish the soundness of Gradual C0's symbolic execution algorithm.That is, we prove that when all run-time checks produced by the symbolic execution algorithm are satis ed, then the program is guaranteed to dynamically execute successfully.A tricky part of this proof is de ning a valuation function [Khoo et al. 2010], which is a partial function mapping symbolic values from symbolic execution to their concrete values for a speci c execution trace from program execution.This function is used to state the correspondence between symbolic and concrete execution states.While we start with Khoo et al. [2010]'s simplistic valuation function, we end up with one that is far more complex as it additionally connects isorecursive symbolic predicates from static veri cation with their equirecursive counterparts in dynamic veri cation and handles global invariants such as separation and access permissions from IDF.This proof technique allows our formal system and reasoning to match the implementation more closely than other techniques such as the evidence calculus used in Garcia et al. [2016].This enables us to explore future developments using either the implementation or formalization, and easily update the other to ensure we remain both implementable and sound.
Finally, we present and discuss a soundness bug we found in Gradual C0 during our proof work and have since communicated to DiVincenzo et al. [2022].The bug is a speci c interaction caused by reducing run-time checks using statically available information in isorecursive predicates, and then checking the remaining run-time checks using equirecursive predicates.This bug could not have arisen in Wise et al. [2020]'s work as their gradual veri cation approach checks all proof obligations at run time.We explore several options for addressing this soundness bug, explain our chosen method in detail, and discuss an implementation x.Despite DiVincenzo et al. [2022]'s thorough empirical evaluation and testing of Gradual C0, this bug was never discovered in their testing.This is likely due to the subtle, intricate interactions between veri cation technologies in gradual veri cation that are hard to test.This demonstrates the value of formally proving soundness in the case of gradual veri cation, and we hope this paper serves as a basis for similar future work.To summarize, this paper makes the following contributions: • Formalization and proof of soundness for Gradual C0, the rst gradual veri er for recursive heap data structures that is based on symbolic execution [DiVincenzo et al. 2022].The level of abstraction chosen for this proof work improves the readability of Gradual C0's design and makes adapting this work to prove other symbolic execution-based gradual veri ers sound much easier.• Formalization of a core subset of the Viper static veri er, which is based on symbolic execution and supports IDF.This work provides the rst solid foundation for proof work on static veri ers that use symbolic execution and IDF.• A re ection on the trial and error of picking the right level of abstraction for our proof work in this paper.• Demonstration of a soundness bug we found in Gradual C0 during our work and have since communicated to DiVincenzo et al. [2022].We also provide several options for addressing this bug and advise on how to implement one of our solutions.

SVL C0
We rst introduce SVL C0 and a corresponding static veri cation algorithm.Since it does not include imprecise speci cations, SVL C0 can be veri ed by existing static veri cation tools such as Viper [Müller et al. 2016].The veri cation algorithm corresponds to the core algorithm of Viper, which is the foundation for static veri cation in Gradual C0.We illustrate how our formalism and soundness result can be applied to Viper.In later sections we extend SVL C0 's veri cation algorithm to support the veri cation of gradually-speci ed GVL C0 programs.

Definition
We de ne an abstract syntax for SVL C0 in Figure 1.Its form is similar to the language of Viper, which is intended for use as a generic backend for multiple frontend languages; however, we use the syntax of C 0 .Programs consist of struct, predicate, and method 1 de nitions, and an entry statement.Struct de nitions contain a list of elds, predicate de nitions contain a parameter list and a formula (the predicate body), and method de nitions contain a parameter list, a return type, a pre-condition (denoted by requires), a post-condition (denoted by ensures), and a statement (the method body).The entry statement represents the body of the main method in traditional C programs.Statements in SVL C0 follow C conventions, except for while, alloc, and return.All while statements specify a formula called a loop invariant, which states the properties preserved by the loop during execution.An alloc statement allocates new memory on the heap, initializes it with a default value, and updates the variable on the left-hand side to contain a reference to the newly allocated value.This matches C 0 semantics, except C 0 returns a pointer, not a reference, and thus the type of the variable is written di erently.We omit return statements; instead, the method body must assign to a special result variable, whose value is then returned after executing the method body.This re ects the behavior of Gradual Viper which also does not have a return statement.Additionally, we simplify several statements to make formal de nitions and proofs easier.For example, assignment only occurs to a variable or a eld of a variable; statements such as x.y.z = 1 are not permitted.We also omit void method calls, since these do not di er meaningfully from calls to value-returning methods.
Like Gradual C0 [DiVincenzo et al. 2022], SVL C0 does not support arrays.Verifying non-trivial properties of programs that use arrays would require signi cant extensions to existing gradual 1 To distinguish them from pure functions (which are used in the speci cation language of similar veri cation tools) we use method to refer to any potentially impure function.veri cation theory -for example, quanti ed formulas.These extensions are left to future work.However, we can verify recursive data structures such as linked lists with abstract predicates.Note that Viper does support quanti ed formulas and arrays, thus further work is necessary to formally prove soundness of these capabilities.
We make several simplifying assumptions for SVL C0 programs.All variables are initialized before they are used, and every execution path for a method body assigns the result variable at its end.Every program is well-typed; that is, expressions used in if conditions or as boolean operands will evaluate to bool values, all arguments passed to method parameters will match the de ned parameter type, and the value assign to result has type equal to the method's return type.Finally, all speci cations (predicate bodies, loop invariants, and method pre-and post-conditions) are self-framed, which is a special well-formedness condition from IDF that we de ne later.
Formulas (speci cations) in SVL C0 are written in the logic of IDF [Smans et al. 2012] and recursive predicates [Parkinson and Bierman 2005].Thus formulas may contain expressions as well as abstract predicates and accessibility predicates from IDF; formulas may be joined by the separating conjunction * [Smans et al. 2012].An accessibility predicate acc( .) requires access to the heap location . .A predicate instance ( ) applies the boolean predicate to the arguments .An expression requires that evaluates to true.A separating conjunction, as in 1 * 2 , acts like a logical AND for 1 and 2 , but also requires the heap locations speci ed by predicates and accessibility predicates in 1 to be disjoint from those speci ed in 2 , e.g.acc( .) * acc( .) implies != .A conditional formula if then 1 else 2 denotes the validity of 1 when evaluates to true; otherwise it denotes the validity of 2 .
Formulas in IDF, and thus in SVL C0 , must be self-framed [Smans et al. 2012], which requires permissions for all heap locations used in a formula to also be in that formula.For example, x.value == 0 is not self-framed since it references the heap location x.value, but does not assert accessibility of the eld x.value.However, acc(x.value)* x.value == 0 is self-framed.We specify rules for framing and self-framing in §4.3.
Static veri cation of predicates is done isorecursively [Summers and Drossopoulou 2013], thus predicate instances must be explicitly folded before they can be asserted.Similarly, predicate bodies must be explicitly unfolded before asserting the implications of a predicate.This enables static veri cation of recursive predicates and simpli es reasoning about the veri er's behavior.

Representation
In this section, we formally de ne the data structures used during static veri cation of SVL C0 programs.
• A symbolic value ∈ SValue is an abstract value representing an unknown value, such as an integer or object reference.We leave the concrete type of SValue unde ned, but assume that an in nite number of distinct new values can be produced by a fresh function.• A symbolic expression ∈ SExpr is a symbolic or literal value, or is composed of other symbolic expressions and operators.symbol or a triple ⟨ , , ˜ ⟩ consisting of a symbolic state , a statement that remains to be executed, and a formula ˜ that must be asserted after executing .(Σ), (Σ), and ˜ (Σ) are used to reference a speci c component of Σ when Σ is not a symbol.
• A valuation : SValue → Value is a mapping from symbolic values to concrete values (de ned in §4.1).Valuations are implicitly extended to be de ned for all SExpr, following the structure of symbolic expressions.

Evaluating Expressions
Symbolic execution evaluates an expression to a symbolic value using the symbolic state , and is denoted by the judgement ⊢ ⇓ ⊣ ′ .It also yields a new symbolic state ′ which may contain a more speci c path condition if this particular evaluation short-circuits a boolean operator.Selected formal rules for symbolic evaluation are given in Figure 2. Literals are evaluated to themselves and variables are evaluated to the corresponding value in the symbolic store.Some operators, such as negation and arithmetic operators, are directly translated into a symbolic expression using the respective operator.In contrast, boolean operators are short-circuiting: when evaluating 1 && 2 , if 1 evaluates to false, then 2 is never evaluated (in this case, 1 == false is added to the path condition).We de ne two non-deterministic rules for each binary boolean operator-SEvalAndA represents the short-circuiting case just described, while SEvalAndB represents the non-shortcircuiting case where 1 is true, so 2 must also be evaluated to determine the result.Finally, eld references are evaluated to the symbolic value contained in their corresponding eld chunk in the symbolic heap.Note, a heap chunk for the eld reference must be in the heap, otherwise evaluation fails (and ultimately static veri cation as well), thus the eld reference must be framed by the current state.
We also de ne a judgment of the form ⊢ ↓ which symbolically evaluates an expression to a symbolic expression without short-circuiting.Thus the judgment is deterministic and does not update the path condition.Instead, logical operators such as && are encoded directly in the symbolic expression (compare SEvalPCAnd with SEvalAndA/SEvalAndB in Figure 2).This results in a less speci c path condition, but reduces the number of execution paths during symbolic execution.This matches the evaluation method described in DiVincenzo et al. [2022] for evaluation in formulas, while the former style is used for evaluation in imperative code.

Consuming Formulas
Given a symbolic state and formula , consuming a formula rst asserts that is established by , and second removes the heap chunks in corresponding to permissions (predicates and accessibility predicates) in .The judgment ⊢ ▷ ′ denotes consumption; i.e., is consumed from , resulting in the new symbolic state ′ .See Figure 3 for selected rules.
Consuming an accessibility predicate such as acc( .) rst asserts the predicate has a corresponding eld chunk in the heap, and second removes the chunk from the heap (SConsumeAcc).Consuming a predicate similarly looks for and removes the corresponding predicate chunk from the heap (SConsumePredicate).If any of the chunks are missing from the heap, then veri cation fails.Expressions must evaluate to true in the current symbolic execution path.That is, the current path condition must imply the symbolic value of the expression (SConsumeValue).As mentioned previously and seen in the aforementioned rule, expressions in formulas are evaluated with the deterministic evaluation judgment (i.e., not the short-circuiting one), which matches the behavior described in DiVincenzo et al. [2022] and reduces the number of branches generated during symbolic execution.This di ers from Viper, which uses a single, short-circuiting eval algorithm everywhere, including in consume.A separating conjunction, such as 1 * 2 , is consumed leftto-right, i.e. 1 is consumed and then 2 is consumed (SConsumeConjunction).This enforces the separation of permissions between the two conjuncts -heap chunks necessary to satisfy the permissions asserted in 1 will be removed before consuming 2 , so, if they overlap, consumption of 2 will fail.Finally, we de ne consumption of logical conditionals, like if then 1 else 2 , in two non-deterministic rules.In SConsumeConditionalA, is assumed to be true in the path condition and 1 is consumed.Likewise, in SConsumeConditionalB, is assumed to be false in the path condition and 2 is consumed.
Note, as we saw in §2.3, evaluation of a eld access in an expression requires the state to contain a heap chunk for the eld.But consume removes heap chunks from the state in a left-to-right manner thanks to rules SConsumeAcc and SConsumeConjunction.For example, we may want to consume the formula acc( .) * .== 0. First, a heap chunk for acc( .) is found and removed from the heap.Then, the resulting state is used to frame and evaluate .== 0 in the next consume step.However, the heap chunk for .was removed from the state so evaluation fails when it shouldn't since the original state contained the heap chunk.To solve this issue, we de ne consume using an underlying judgment, denoted , ⊢ ▷ ′ , which asserts and removes permissions from while evaluating expressions with the unchanging reference state .The state is the symbolic state before consumption.The rule SConsume de nes the top-level consume judgment using this new underlying judgment.
Our consume judgment represents the core functionality of DiVincenzo et al. [2022] and Schwerho [2016]'s consume algorithms.We, of course, ignore unnecessary implementation details like snapshots, which preserve certain portions of the state that are removed during consume.

Producing Formulas
Given an initial state and formula , producing adds the information in into the symbolic state , resulting in a new state ′ .The judgment for ⊢ ◁ ′ denotes production; i.e., is produced into the state , resulting in ′ .In particular, produce adds heap chunks representing predicates in to the symbolic heap and symbolic expressions representing constraints from boolean expressions in to the path condition in a left-to-right manner.Note, each symbolic heap chunk represents a distinct region of memory at run-time, an invariant that we later prove.Thus overlapping heap chunks may only occur in symbolic states which represent an unreachable dynamic state and can safely be ignored.When producing formulas, we use deterministic symbolic evaluation for expressions, but we introduce separate execution paths for conditionals (similar to §2.4).
Formal rules are given in the supplement [Zimmerman et al. 2024].These rules capture the functionality of the produce algorithm speci ed in DiVincenzo et al. [2022] and Schwerho [2016].As noted in the previous section, Schwerho [2016] uses short-circuiting evaluation in all places, while we use deterministic evaluation.

Executing Statements
Now that we have formally de ned symbolic execution of expressions and formulas, we can put the pieces together to de ne symbolic execution of program statements.
We represent the symbolic execution of program statements as small-step execution rules denoted by the judgment ⊢ → ′ ⊣ ′ , where the initial statement is symbolically executed with the initial state , resulting in the state ′ , and then transitions to the next statement ′ with the new state ′ .Selected formal rules are shown in Figure 4. Executing a variable assignment updates the symbolic store (SExecAssign); while executing a eld assignment rst consumes acc( .), and then adds a new heap chunk for .to the heap that contains .'s new symbolic value after the write (SExecAssignField).An alloc( ) statement adds a heap chunk for each eld in to the symbolic heap.The new object reference is a fresh value but the new eld chunks are each initialized with default values, which re ects the behavior of C 0 (SExecAlloc).Execution rules for if statements are non-deterministic: given a statement if then 1 else 2 , SExecIfA adds to the path condition and continues execution with 1 , while SExecIfB adds ! to the path condition and continues execution with 2 .Symbolic execution of method calls is modular; i.e., the behavior of the method call is represented by the method's pre-and post-conditions (SExecCall).First, the method's arguments are evaluated to symbolic values.Then the pre-condition is consumed using a special environment containing the argument values.A fresh symbolic value is added to represent the return value of the method, and then the post-condition of the method is produced.The special environment is then replaced by the original environment, with the addition of the result's symbolic value.Loops (i.e while statements) are executed similarly: the loop invariant is consumed, variables modi ed by the loop body are set to fresh values in the symbolic store, the loop invariant is produced, and the negated loop condition is added to the path condition (SExecWhile).Execution of the fold and unfold statements is also similar to loops and method calls: fold consumes the predicate body and adds a representative predicate chunk to the symbolic heap, while unfold consumes the predicate instance (thus removing the predicate chunk from the heap) and produces the predicate body.

Modularly Verifying Programs
We now de ne veri cation of entire programs.We start by de ning what a program Π is; it is a quadruple ⟨ , , , ⟩ where is the entry statement of the program, is the set of method names, is the set of predicate names, and is the set of struct names in the program.Then, we de ne the judgment Π ⊢ Σ → Σ ′ that speci es all possible symbolic execution steps that occur during veri cation of Π. Selected rules are given in Figure 5.
A veri cation state Σ is reachable from program Π if Σ = init or Π ⊢ Σ 0 → Σ for some reachable Σ 0 .The latter judgement only holds when Σ 0 is itself reachable.
This judgement includes rules for modular veri cation.From init, we can begin veri cation of the entry statement (SVerifyInit) or of any method (SVerifyMethod).When verifying a method, the SVerifyInit ⟨ , , , ⟩ ⊢ init → ⟨ empty , , true⟩ method's post-condition is used as the formula of the veri cation state.After completely executing the method's body, i.e. having reached skip, we consume the formula contained in the veri cation state (SVerifyFinal), which is the method's post-condition.We modularly verify loop bodies following a similar pattern.As described in §2.6, symbolic execution steps over loop bodies in the same way it steps over method calls.However, we introduce a veri cation rule (SVerifyLoopBody) that allows symbolic execution of a loop body, beginning with a new symbolic state.We reuse the symbolic store from the initial symbolic state, except that all variables modi ed by the loop body are replaced by fresh values.Veri cation proceeds similar to method veri cation, except that we use the loop invariant for the formula of the new veri cation state-we produce the loop invariant, symbolically execute the loop body, and nally consume the loop invariant.Thus symbolic execution, which steps over the loop, ensures that the loop invariant holds for the initial iteration, while this veri cation rule ensures that the loop invariant is preserved after every iteration.
We also include another veri cation rule for loops, SVerifyLoop, in order to match the behavior of Gradual C0.This rule and its correspondence with Gradual C0 is described further in §7.2.

Example
We now illustrate veri cation of the append method de ned in Figure 6, which appends a given value to the end of a list using recursion.The append method is ensured to be memory safe and preserve acyclicity of the list through veri cation.We begin with an empty state and initialize all parameters with fresh values: Then the pre-condition acyclic(l) * l != NULL is produced: Unfolding acyclic(l) (line 17) consumes the predicate from the state and produces its body.The body of acyclic(l) contains a logical conditional resulting in two possible execution paths for produce -one where 4 is null and one where 4 is not null, where 4 is the symbolic value for l.next: We follow both execution paths, using color-coding to distinguish them.Next, when executing the if statement (line 18), we rst evaluate the condition.Since l.next is framed by the state, evaluation of the condition succeeds and execution branches along the if.We rst consider executing the then branch of the if, where 4 == null is added it to the path condition: However, the path condition 1 != null && 4 != null && 4 == null is unsatis able, thus we can safely prune this execution path and only continue with the rst.We proceed to symbolically execute the call to singleton (line 19) by consuming the (empty) pre-condition, and producing the post-condition.The result is represented by a fresh symbolic value 5 : 19 n = singleton ( value ); Symbolic execution of this path then jumps to line 22, but to preserve code order we now demonstrate veri cation of the else branch (line 20).To do this, we use states 3 and 3 , and add the negation of the condition to verify the else body: Here again this results in an unsatis able path condition 1 != null && 4 == null && 4 != null, so we prune that path.We continue with the other path and execute the recursive call to append (line 21), which consumes the pre-condition (removing ⟨acyclic, 4 ⟩) and produces the post-condition, using the fresh value 6 to represent the result (adding ⟨acyclic, 6 ⟩): Now we have completed verifying both branches of the if statement.Note that we do not actually join execution at this point; instead, we jump to line 22 immediately after executing the program up to 5 and ′ 5 along both paths.We follow both of these paths for the rest of veri cation.The eld assignment on line 22 consumes acc(l.next)and produces a new corresponding heap chunk with n's value: Folding acyclic results in twice the number of execution paths since it consumes acylic(l)'s body, which includes an logical conditional.However, again, information from the path conditions in 6 and ′ 6 allow us to prune some of these paths.We elide these pruned paths and only show the taken ones.After consuming acyclic(l)'s body, execution produces acyclic(l) into the state: 23 fold acyclic ( l ); Finally, in both 8 and ′ 8 , we can consume the post-condition acyclic(result) * result != NULL.Therefore, we have veri ed all possible symbolic execution paths of append's body, and thus veri ed append.
3 GVL C0 SVL C0 re ects the core components of Viper-eval, consume, produce, and exec.We now formally de ne GVL C0 , an extension of SVL C0 which supports gradual speci cations.We then de ne static veri cation for GVL C0 that allows optimistic assumptions to satisfy proof goals and generates checks to be veri ed at run time to cover these assumptions as in DiVincenzo et al. [2022].
Note, the syntax of GVL C0 di ers slightly from that of GVC0 (the frontend for Gradual C0), particularly with its omission of C-style pointers.However, due to the restrictions of C 0 , all usages of pointers in C 0 can be translated to use object references.This and other translations are done by Gradual C0 during its conversion to an intermediate language Gradual Viper, which is used in the backend veri er.In order to simplify our model, GVL C0 is very similar to the language of GVC0, but incorporates elements of the Gradual Viper language when this simpli es the de nition of our veri cation algorithm.

Gradual Formulas
We rst extend the syntax of our language to include imprecise formulas-formulas of the form ? * .An imprecise formula may represent any logically consistent strengthening of the precise portion [Wise et al. 2020].For example, the imprecise formula ?* > 0 consistently implies == 2, but does not consistently imply == 0.Then, a gradual formula ˜ may be precise or imprecise, and gradual programs are programs that contain gradual formulas.The abstract syntax of GVL C0 extends SVL C0 's syntax with gradual formulas: Note, imprecise formulas are always considered self-framed, because they can always be strengthened to be self-framing.Therefore we require all method pre-and postconditions, loop invariants, and predicate bodies to be speci cations-formulas which are either imprecise or self-framed.
Also, note that IDF is particularly well-suited for gradual speci cations, in comparison to separation logic [Reynolds 2002], since IDF allows separately specifying access permission and heap values.This allows speci cation of heap values while leaving more complex accessibility assertions unspeci ed, as in the formula ?* x.f != null.

Representation
In this section we extend the data structures from §2.2 to support imprecise states-states in which it is permissible to make optimistic assumptions-and de ne our representation of run-time checks.
• A symbolic state is now a quintuple ⟨ , H, H, , ⟩ where is an imprecise ag, H is a precise heap, H is an optimistic heap, is the symbolic store, and is the path condition.As before, we use the notation ( ), H( ), etc. to reference speci c components of a symbolic state.and are de ned in §2.2 but we rede ne the other components.• An imprecise ag ∈ {⊤, ⊥} is a ag indicating whether a symbolic state is imprecise (⊤) or precise (⊥).( ) denotes that ( ) = ⊤ (and thus is an imprecise state), while ¬ ( ) denotes that an ( ) = ⊥ (and thus is precise).Imprecise states are produced by consuming or producing an imprecise speci cation.Once imprecise, a state always remains imprecise.• A precise heap H is a symbolic heap as described in section 2.2.Thus it is a nite set of heap chunks where all heap chunks represent distinct locations in the heap at run time.• An optimistic heap H is a nite set of eld chunks.Field chunks contained in the optimistic heap may represent the same location in the heap at run time, i.e. the optimistic heap does not preserve the separation invariant like the precise heap.The optimistic heap of a well-formed symbolic state must be empty unless it is an imprecise state.

Run-Time Checks
A run-time check ∈ SCheck denotes an assertion that validates assumptions made during static veri cation of imprecise programs.It is a symbolic expression, symbolic permission, pair of symbolic permission sets, or ⊥: A set of run-time checks is denoted R ∈ P (SCheck).In a run-time check, a symbolic expression asserts that the value represented by at run time is true, a symbolic permission asserts ownership of a eld or a predicate instance, and a pair sep(Θ 1 , Θ 2 ) asserts that the sets of permissions represented by Θ 1 and Θ 2 are disjoint.⊥ represents a static veri cation failure.We represent static veri cation failure as an unsatis able run-time check, instead of failing veri cation entirely, to accommodate imprecision.
Note that our run-time checks contain symbolic values.This is unlike Gradual C0 [DiVincenzo et al. 2022], where checks produced have their symbolic values replaced by corresponding program variables.This replacement is needed to support the implementation of run-time checks and adds a signi cant amount of complexity to their algorithms.Fortunately, as we will see later, we can abstract away this connection of symbolic values to program variables (aka.concrete values) using valuations; and so we can produce abstracted checks here, avoiding additional complexity in our formalism.Additionally, at each branch point DiVincenzo et al. [2022] check whether all possible branches fail and, if so, halt static veri cation.We do not specify this behavior; however, this is possible by checking for ⊥ ∈ R at each step of symbolic execution.

Evaluating Expressions
We now extend our previous judgement for symbolic evaluation from §2.3 to allow optimistic symbolic evaluation of expressions.We specify a set R of run-time checks necessary for a given evaluation, thus our judgement is now ⊢ ⇓ ⊣ ′ , R. Field chunks may be referenced in the optimistic heap by SEvalFieldOptimistic in Figure 7.These eld chunks have already been validated, thus we do not need additional run-time checks.A eld may also be optimistically evaluated by SEvalFieldImprecise, even if it does not exist in H or H .This adds a new eld chunk with a fresh value to H .This requires a run-time check which asserts permission to access the eld.Finally, SEvalFieldFailure applies in a precise state when a eld is referenced but no matching heap chunk exists.This results in a failure of static veri cation, represented by ⊥, for that execution branch.
We also modify the existing set of rules described in §2.3 to collect run-time checks from recursive evaluations.Likewise, we modify the deterministic evaluation judgement to add similar rules as those described above, allowing it to also generate run-time checks, thus its form is ⊢ ↓ ⊣ R.

Consuming Formulas
We extend our previous judgment for consuming formulas from §2.4 to handle imprecise formulas and imprecise states.As in §3.4,we add a parameter R to the consume judgments.Additionally, we collect all permissions for the given formula into a set of symbolic permissions Θ so that separation checks may be added where necessary.Thus the new judgments are of the form ⊢ ˜ ▷ ′ , R and , ⊢ ˜ ▷ ′ , R, Θ; these two forms are related by SConsume and correspond to the forms described in §2.4.We list selected rules in Figure 8.
Consuming an imprecise formula empties the precise and optimistic heaps (SConsumeImprecise).This is because the imprecision may represent access to arbitrary elds.For example, a method with an imprecise precondition could modify any eld that the callee owns, thus we cannot make any assumptions about eld permissions or values after the method returns.Consuming an imprecise formula results in an imprecise state, thus removed eld chunks can be referenced optimistically, with the possible addition of a run-time check.
We must also consider the case of consuming an imprecise formula in a precise state.Since optimistic assumptions are not permitted in a precise state, we cannot assume any of the assertions contained in the imprecise formula.However, the imprecise formula may reference elds without a corresponding accessibility predicate.Thus, when consuming an imprecise formula, we use an imprecise state as the symbolic state for evaluation, but use the original (possibly precise) state for assertions.
In an imprecise state we may optimistically consume an expression, even if it is not implied by the current path condition.We then add the value as a run-time check to be asserted at run-time.
Consumption of accessibility predicates must be modi ed to handle imprecise states, where eld chunks in H may overlap with eld chunks in H.We must remove all elds that may represent the same heap reference when removing a eld chunk from H. To do this, we use the helper functions rem fp and rem f .rem fp is used when removing heap chunks from the precise heap.For precise states, rem fp removes the eld chunk that coincides exactly with the heap location being consumed (thus computing the same result as the rules in §2.4).For imprecise states, it also removes all eld chunks that could possibly coincide with the speci ed heap location.rem f is used when removing chunks from the optimistic heap and behaves similarly, but also removes all predicate chunks, since predicates occurring in the precise heap could overlap with heap chunks in the imprecise heap.Some optimizations could be made -for instance, if a predicate's unfolding will never reference a eld , we could preserve an instance of this predicate when consuming acc( .).However, we leave such optimizations to future work.
We can also optimistically assume an accessibility predicate in an imprecise state, even if a matching eld chunk does not exist in H or H . Since this assumes ownership of the eld, we add the corresponding symbolic permission to R. Finally, like accessibility predicates, we allow optimistic consumption of predicate instances.In this case the symbolic permission representing the predicate instance is added as a run-time check.
When consuming any accessibility predicate or predicate instance, the symbolic permission is always added to a set Θ.This allows speci cation of checks for separation.When consuming acc( .) * acc( .), if acc( .) is optimistically assumed while acc( .) is statically veri ed, the run-time check for acc( .) does not imply that its permission is disjoint from that of . .Therefore additional checks for separation are added when consuming a separating conjunction such as 1 * 2 .If no run-time check for permissions exists, all permissions must have been consumed from H or H and thus separation may be assumed.However, if a symbolic permission is contained in R we can no longer assume separation.Thus we add a run-time check sep(Θ 1 , Θ 2 ) where Θ 1 is the set of symbolic permissions collected while consuming 1 and likewise for Θ 2 and 2 .

Producing Formulas
Since a formula is only produced when we can assume its validity, producing a gradual formula does not require any optimistic assumptions, thus we do not need to calculate any run-time checks.When producing an imprecise formula, we produce the precise portion and set = ⊤.All other rules from §2.5 are left unchanged.

Executing Statements
All rules from §2.6 are left unchanged.While it may seem natural to calculate run-time checks while determining execution transitions (as in the exec algorithm of DiVincenzo et al. [2022]), we found that this unnecessarily complicates statements of soundness since symbolic execution steps are not equivalent to dynamic execution steps.For example, a method call occurs in one step during symbolic execution but may never complete during dynamic execution, therefore it may be impossible to determine which symbolic execution step applies.However, assertion of run-time checks must occur before a dynamic execution step may proceed.Therefore we cleanly delineate between symbolic execution transitions, speci ed by the judgement ⊢ → ′ ⊣ ′ , and the calculation of run-time checks.

Guarding Execution
As described above, we must the assert run-time checks before the corresponding dynamic execution step occurs.Therefore we de ne guard judgements to calculate run-time checks which ensure that execution can safely proceed.A guard for a method call calculates the checks necessary to ensure that the method's pre-condition is satis ed, while a guard for a eld assignment calculates the checks necessary to ensure permission to access the assignee and evaluate the value to be stored.
A guard judgement Σ ⇀ ′ ⊣ R, Θ denotes that, at the execution state represented by Σ, when the execution path matches the path condition in ′ , the run-time checks R must be checked.Selected guard rules are de ned in Figure 9.
In a guard judgement, Θ determines the exclusion frame-a set of permissions which must not escape the executing method's context.Its necessity and behavior is explained in §8.

Example
We now illustrate veri cation of the gradually-speci ed method in Figure 10.We assume the de nition of List and acyclic from Figure 6.The gradual speci cation of append ensures that all returned lists are acyclic.
Symbolic states are tuples of the form ⟨ , H, H, , ⟩.As in §2.8, we begin veri cation of append by assigning fresh symbolic values to all parameters and producing the pre-condition ?* true, which results in an imprecise state: At each statement we compute the guard to nd the necessary run-time checks.The guard for the if statement at line 5 evaluates l.next == NULL.A heap chunk for l.next is optimistically added to H with a fresh value 3 .This also results in a run-time check for the symbolic permission ⟨ 1 , next⟩.
The next state 2 is computed by symbolic execution.This again evaluates l.next == NULL in the state 1 , which again requires the addition of an optimistic heap chunk.Since 3 was not previously used in 1 it can be used again as a fresh value for symbolic execution.
The guard for line 6 consumes the pre-condition of singleton, which requires no run-time checks.Symbolic execution consumes the same pre-condition and produces the post-condition; here we use the fresh value 4 for the returned value: As in §2.8, we follow code order, instead of following each execution path individually, and distinguish separate execution paths with color-coding.The guard at line 5 computes the checks for both branches of if, thus the guard is not computed at line 7.We can symbolically execute the else branch by adding the negation of the path condition we used previously: The guard for line 8 consumes the pre-condition of append, which is ?* true.Since the body of this imprecise formula is only true, no run-time checks are necessary.However, since this is an imprecise formula, we clear the precise and optimistic heaps (SConsumeImprecise in Figure 8).Symbolic execution then produces the post-condition; here we use the fresh value 5 for the returned value: We resume symbolic execution of both paths at line 9. Executing l.next = n in the state 3 does not require any run-time checks since it contains the heap chunk representing l.next.However, executing the same statement in 3 requires optimistic assumption of the symbolic permission ⟨next, 1 ⟩ which requires a run-time check and removes the predicate instance.DiVincenzo et al.
[2022] describe the implementation of such conditional run-time checks, but here we represent it with separate symbolic execution paths: Finally, the applicable guard at line 11 consumes the post-condition acyclic(result).Since neither state contains a matching predicate instance, in both paths a run-time check is added for the symbolic permission ⟨acyclic, 1 ⟩: Now we have veri ed the method and computed all necessary run-time checks.

EXECUTING GVL C0
Since soundness of static veri cation requires speci cation of program execution, we de ne execution semantics for GVL C0 programs (including SVL C0 programs, which are a subset).This includes dynamic semantics for formulas, and execution semantics which dynamically assert the validity of every speci cation.Therefore, these semantics de ne valid execution for GVL C0 programs.
As explained in §1, execution semantics are based on those of C 0 [Arnold 2010], while the semantics of formulas are based on those of GVL RP [Wise et al. 2020].

Representation
In this section, we formally de ne the data structures used during execution of GVL C0 programs: • A value ∈ Value is an integer, boolean, or object reference.
• An object reference ℓ ∈ Ref is an identi er for a particular object.As with symbolic values, we assume that an in nite number of distinct values can be generated by the fresh function.The type of value represented by fresh is disambiguated by its usage.• An environment is a partial function mapping variable names to values, i.e. : Var ⇀ Value.
• A heap : Ref × Field → Value is a function mapping object reference and eld pairs to values.We assume that the heap function is total, i.e. all reference and eld pairs have some corresponding value, but heap access is restricted during execution by a set of access permissions ∈ P (Ref × Field).This re ects the semantics of IDF [Smans et al. 2012].A heap location ⟨ℓ, ⟩ is owned if it is contained in the currently-applicable set of access permissions.
• A stack frame is a triple ⟨ , , ⟩ containing of a set of owned permissions , a local environment , and a statement .A stack S is a list of stack frames -either ⟨ , , ⟩ • S for some other S, or the empty stack, denoted nil.For a non-empty stack S, (S), (S), and (S) refer to their respective components of the head element.
• A dynamic state Γ may be a symbol init or final, or pair ⟨ , S⟩ containing a heap and a non-empty stack S. (Γ) and S(Γ) reference individual components of Γ when Γ is not a symbol, while (Γ), (Γ), and (Γ) reference a component of the head element of S(Γ).(init) and (init) are de ned to be ∅.

Evaluating Expressions
Given a heap and environment , the evaluation of expression to a value is represented by a judgement of the form ⟨ , ⟩ ⊢ ⇓ .This follows standard evaluation rules-variables are evaluated to the corresponding value in and eld references are evaluated to the corresponding value in .The boolean operators && and || are short-circuiting-when evaluating 1 && 2 , 2 is only evaluated when 1 is not true.

Asserting Formulas
A judgement of the form ⟨ , , ⟩ ⊨ ˜ denotes that a gradual formula ˜ is satis ed given a heap , a set of accessible permissions , and an environment .Selected rules are shown in Figure 11.Boolean expressions are satis ed when they evaluate to true.An accessibility predicate acc( .) is satis ed when the eld referenced by . is in the set of accessible permissions.A predicate instance ( ) is satis ed when the predicate body is satis ed using an environment mapping each predicate parameter to the corresponding argument .A separating conjunction 1 * 2 is satis ed when 1 is satis ed using a permission set 1 and 2 is satis ed using a permission set 2 , where 1 and 2 are disjoint subsets of .Finally, an imprecise formula ?* is satis ed exactly when is satis ed.
A judgement of the form ⟨ , , ⟩ ⊢ frm denotes that the expression is framed by the given set of permission .This denotes that all heap locations necessary to evaluate are included in .Selected rules are shown in Figure 11.
Note that a predicate instance ( ) is satis ed i the predicate body is satis ed.Thus dynamic execution of GVL C0 uses equirecursive semantics for predicates [Summers and Drossopoulou 2013].We also de ne equirecursive framing of formulas by the judgement ⟨ , , ⟩ ⊢ frmE ˜ .A formula is equirecursively framed if its unrolling, the recursive expansion of referenced predicate bodies, is framed.
As speci ed in §3.1, a speci cation is a formula which is imprecise or self-framed.

Footprints
The footprint of a formula is the set of permissions necessary to assert a formula [Reynolds 2002].There are two types of footprints for gradual formulas: The exact footprint of a formula is the minimal set of permissions necessary to assert and frame a formula.Given a heap and environment , ˜ ⟨ , ⟩ denotes the exact footprint of a formula ˜ .
The maximal footprint (often abbreviated to footprint) of a formula contains the exact footprint and all permissions that are consistently implied by the formula.The maximal footprint of a completely precise formula is its exact footprint, but the maximal footprint of an imprecise formula contains all accessible permissions.Given a heap , set of owned permissions , and environment , ⌊ ˜ ⌋ ⟨ , , ⟩ denotes the maximal footprint of a formula ˜ .

Executing Statements
We represent the dynamic execution of program statements as small-step execution semantics denoted by the judgement ⟨ , S⟩, ˆ → ⟨ , S⟩, where the statement is executing with the initial state ⟨ , S⟩, and then transitions to the next statement ′ with the new state ⟨ , S⟩. ˆ speci es the exclusion frame, which is described below.Selected rules are shown in Figure 11.Execution will be stuck (i.e., no further derivation will apply) when an error is encountered.For example, execution is stuck when a formula is not satis ed or if some expression is not framed.A method call is executed by evaluating all arguments, asserting the pre-condition, and adding a new stack frame containing the footprint of the pre-condition and the method body.After the method body is completely executed, the post-condition is asserted and the result value in the callee's environment is passed to the caller's environment.
A loop is executed similarly to a method, but uses the loop invariant instead of a method contract.When the loop condition is true, an iteration is executed by asserting the invariant and adding a new stack frame for the loop body.When the body is complete, we return to the original loop statement, allowing further iterations as long as the condition remains true.When the condition is false, the invariant is still asserted but execution skips over the statement.These rules are speci ed in the supplement [Zimmerman et al. 2024].
ˆ speci es the exclusion frame -a set of permissions which may not be passed to the callee or loop body.It is used only for executing method calls and loops.We later explain why this is necessary for soundness in §8.
fold and unfold statements are ignored at run-time.Explicit folding and unfolding of predicate instances is not required because the run-time uses equirecursive semantics for predicates.
The entire set of possible execution steps for a program Π is determined by judgements of the form Π ⊢ Γ, ˆ → Γ ′ , which denote that execution transitions from Γ to Γ ′ , using the exclusion frame ˆ .From the init state, execution may only step to the entry statement, and then execution follows the rules described above.

CORRESPONDENCE
Before formalizing soundness, we must specify the correspondence between veri cation and dynamic states.We include invariants which depend on concrete values, such as separation, in this correspondence relation.Finally, we specify the behavior of run-time checks in a dynamic state.

State Correspondence
A dynamic environment models a symbolic store via a valuation when .This denotes that ∀ ↦ → ∈ : ↦ → ( ) ∈ .
The footprint of a heap chunk, given valuation and heap , is denoted ℎ .The footprint of a eld chunk ⟨ , , ′ ⟩ is {⟨ ( ), ⟩}.The footprint of a predicate chunk ⟨ , ⟩ is the exact footprint of the predicate when applied to the arguments ( ). and model an optimistic heap H when ⟨ , ⟩ H .This has the same requirements as that for H, except that heap chunks are allowed to overlap.
, , and model a symbolic state when ⟨ , , ⟩ .This denotes that and model both H( ) and H ( ), models , and the path condition is true-( ( )) = true.
We also refer to these relations as correspondence-⟨ , , ⟩ denotes that the symbolic state corresponds to , , and .
Finally, a veri cation state Σ corresponds to a dynamic state Γ with valuation if Σ = Γ (i.e., Σ and Γ are the same symbol), or ⟨ ( ), (Γ), (Γ)⟩ (Σ) and (Γ) = (Σ).In other the heap and head stack frame of Γ model the symbolic state of Σ, and the statement in the head stack frame is syntactically the same statement as that of Σ.

Run-Time Checks
We also de ne the semantics of run-time checks using valuations.The judgement ⟨ , ⟩ ⊢ denotes the assertion of a run-time check , given a valuation , a heap , and a set of owned permissions .Likewise, ⟨ , ⟩ ⊢ R denotes the assertion of all run-time checks contained in R. Formal rules are given in Figure 12.

SOUNDNESS
We can now state the soundness of our static veri er.We slightly modify a traditional progress/preservation statement of soundness in order to accommodate run-time checks.

Corresponding Valuations
For most symbolic execution judgements, we de ne a corresponding valuation, inspired by the valuations used in Khoo et al. [2010].This de nes how symbolic values used in the judgement are mapped to concrete values.To calculate the corresponding valuation we require an initial valuation, which de nes the valuation for all symbolic values contained by the input symbolic state, and a dynamic heap, which de nes the valuation for optimistically-added elds.A corresponding valuation ′ must extend the initial valuation , i.e. ′ ( ) = ( ) for all ∈ dom( ).
We denote the corresponding valuation for a judgement J , initial valuation , and heap by [J | ].The de nition for each judgement type is de ned in the supplement [Zimmerman et al. 2024], along with the proofs for that judgement.Each corresponding valuation is de ned by induction on the judgement derivation, specifying the corresponding valuation for each derivation rule.The judgment is nondeterministic if only the input state is considered, but knowing the output state resolves this nondeterminism.When the judgement and heap are clear from context, we simply reference the corresponding valuation extending .

Valid States
A valid state is a dynamic state which is completely characterized by veri cation states.If Γ = init this is trivially true.For a dynamic state ⟨ , S⟩, we require that the head stack frame corresponds to a reachable veri cation state.We also require that all other stack frames are partially validated by some reachable veri cation state.
If a stack frame is executing a method call, partial validation is characterized by the stack frame and heap modeling a reachable symbolic state for that program point, with the callee's precondition consumed.For the full de nition refer to Zimmerman et al. [2024].

Progress and Preservation
Our statement of progress is split into two parts.First, theorem 1 states that if Γ is a valid and Γ satis es the run-time checks calculated by a guard with a path condition that matches the current dynamic state, then dynamic execution proceeds.Second, theorem 2 states that we can always nd the guard necessary to apply theorem 1-a guard whose path condition matches.Thus, theorem 2 represents completeness of symbolic execution with respect to possible dynamic execution paths.Together these theorems show that, in a valid state, the only possible way for execution to be stuck is when the run-time checks cannot be asserted.
Finally, our statement of preservation (theorem 3) assumes the antecedent and conclusion of theorem 1-the initial state is valid and satis es the run-time checks of some matching guard-as well as a dynamic execution step to Γ ′ .By theorem 2, we know that there is such a guard statement; i.e., we can always nd the necessary set of run-time checks.Then preservation states that the resulting dynamic state Γ ′ is also valid.
Note that our assumptions for preservation require dynamic execution to not only assert the run-time checks represented symbolically by R, but also respect the exclusion frame represented symbolically by Θ.The necessity and implications of this requirement are discussed in §8.
Taken together, these theorems demonstrate that dynamic execution will never be stuck as long as the run-time checks calculated by static veri cation succeed.Further, it shows that we calculate run-time checks for all possible execution paths.Since the dynamic execution semantics ensures all necessary speci cations are satis ed, this implies that the calculated run-time checks are su cient.

CHALLENGES TO FORMALISM OF STATIC VERIFICATION
Our speci cation of static veri cation in §2 and §3 is formalised using non-deterministic inference rules.This di ers greatly from the speci cations of Schwerho [2016] andDiVincenzo et al. [2022], which both use a CPS-style de nition for algorithms.The latter form is useful when specifying an implementation, but makes it di cult to formulate a syntactic soundness proof.Furthermore, operational semantics allow a higher level of abstraction than pseudo-code de nitions.However, we must carefully consider whether our operational semantics represent the system which is implemented.

Previous Approaches
During development of our soundness proof, we attempted several formulations of soundness.Initially we abstractly de ned a symbolic stack-a list of symbolic states with the form of a dynamic stack.This approach allowed us to easily state correspondence of the entire dynamic state-each dynamic stack frame models a corresponding symbolic stack frame.
We found it challenging, however, to prove that this correspondence is maintained.When the dynamic stack takes a step, we must verify that there is a corresponding symbolic stack.To address this issue, we de ned an execution semantics for symbolic stacks.Unfortunately, this increased the distance between our formalism and implementation, and now we also need to show that all symbolic states are reachable during static veri cation.Perhaps due to the complexity of this approach, the proof of correspondence remained quite di cult even after de ning this execution semantics.
Instead, we de ned a valid state primarily by the correspondence of the currently executing dynamic stack frame with some reachable symbolic state-a symbolic state which is computed during static veri cation, with no input from dynamic execution.This resulted in a much simpler de nition of valid state.
However, in order to completely prove preservation, we also must specify the behavior of intermediate stack frames -frames contained in the dynamic stack below the currently-executing frame.Thus we provide a recursive de nition for a valid partial state.For intermediate frames containing a method call that is waiting to complete, this requires the frame to model a symbolic state that results from consuming the callee method's pre-condition from a reachable veri cation state.We use this to prove that the dynamic state after the method returns models the symbolic state after symbolically executing the method call.

Verification of Loops
Almost all of our symbolic execution rules are nitely non-deterministic.That is, given an input state, there are a nite number of derivations that can apply.This is necessary since all possible states must be computed during static veri cation.
While this property matches the nite branching of symbolic execution, we make an exception in the case of loops-speci cally, the SVerifyLoop rule (Figure 5).It consumes the loop pre-condition, havocs all variables modi ed by the loop body (i.e., replaces them with fresh values), and produces the loop post-condition.Thus it replaces all symbolic values that could be modi ed by the loop body with fresh values.The loop is left in place, which means that the rule can be immediately applied again to derive yet another state.However, this is harmless because repeated applications of this rule result in isomorphic symbolic states-states which represent the same state but with di erent symbolic values.Since the exact symbolic values do not matter, these are equivalent states from the perspective of static veri cation.Therefore, even though we allow unbounded non-determinism, an implementation such as Gradual C0 can compute all possible states (as determined by our formal model) up to this equivalence.In other words, unbounded non-determinism is an artifact of our formalization that does not a ect an implementation.
This exception is motivated by a disconnect between our formal model and the implementation of Gradual C0 [DiVincenzo et al. 2022].In our formalism, run-time checks are computed as symbolic values and lack a representation in terms of the source.Furthermore, we interpret these run-time checks by means of the valuation function, which we only extend with fresh values as dynamic execution proceeds.Therefore, the references in a run-time check are xed -for example, the validity of a check does not change when the heap is updated, since the heap reference has already been fully evaluated against the symbolic heap.are included in the frame of set during dynamic execution are not removed by consume during symbolic execution, thus symbolic execution does not accurately represent dynamic execution.

Possible Solutions
At rst this appears to be an error of static veri cation, and thus we could address this by making static veri cation more conservative.More speci cally, we could require a stronger invariant of the precise heap: the maximal footprint represented by predicate chunks cannot overlap.This contrasts with our current de nition, where the exact footprint represented by a predicate chunk must be disjoint from that of all other predicate chunks.
For example, we could clear the symbolic heaps when consuming any formula that is not completely precise (i.e., the recursive unfolding contains an imprecise formula).When this occurs, we would also need to use an imprecise state, so that the existence of the removed permissions can be optimistically assumed.This would result in empty symbolic heap after line 17 in Figure 14, and a run-time check for the value of result would be required before returning from test, thus soundness is preserved.
This would allow maximal footprints of predicate chunks to overlap in the symbolic heap, but when consuming a predicate instance, all potentially overlapping predicate chunks would be removed.Thus, after some predicate instance is consumed, its maximal footprint would not overlap with any permission represented by a heap chunk contained in the symbolic heap.
Alternatively, we could achieve soundness by removing any predicate instance that is not completely precise when additional permissions are added to the precise heap.Similar to the previous option, we would also need to use an imprecise state when this occurs.In the example, that would (perhaps unintuitively) remove the imprecise() predicate when adding permissions for the alloc statement.This would ensure that the maximal footprint of heap chunks in the symbolic heap never overlap.
Unfortunately, both of these options reduce the number of assertions that can be statically discharged when verifying gradual programs, thus more run-time checks would be necessary.Furthermore, the run-time checks require checking a predicate instance, which can be quite costly since this traverses the entire unfolding of the predicate.
Furthermore, allowing the predicate instance folded at line 14 to a ect permissions allocated afterward, at line 15, seems counter-intuitive.This invalidates the intuitive assumption that the set of permissions represented by a folded predicate instqance will not change while it remains folded.Furthermore this behavior breaks the semantics of ?, as speci ed in Wise et al. [2020], since no logically consistent strengthening of the imprecise predicate allows it to include permissions allocated after its body is folded.
This indicates that the semantics of dynamic execution should be modi ed to exclude access permission for c.value, which is allocated after imprecise() is folded, from being passed to set, which is a precise formula that only requires imprecise().Then execution would fail at line 8 in Figure 14.To accomplish this, we have introduced the concept of an exclusion framea set of permissions which cannot be passed to a callee.This exclusion frame is calculated by symbolic execution, and passed to dynamic execution in much the same way as run-time checks.It is represented by Θ in the guard judgement ( §9), which also calculates R, and is translated to dynamic permissions using a valuation.
The guard rules in Figure 9 calculate the exclusion frame by the rem helper function, after consuming the pre-condition of a method.If the pre-condition is completely precise, then Θ = ∅, thus execution of an SVL C0 program is not a ected.Otherwise, Θ contains all permissions currently contained in the symbolic heaps.In Figure 14, since the pre-condition of set is not completely precise, Θ = {⟨ 1 , value⟩} when calculating the guard statement at line 17.At run-time this is translated to ˆ = {⟨ℓ, value⟩} where c ↦ → ℓ.Then all permissions except ⟨ℓ, value⟩ are passed to set.Thus the run-time check for acc(c.value)at line 8 cannot be asserted.
This addresses the intuitive and semantic problems described above.The isorecursive instance of imprecise referenced in the pre-condition of set should not represent access to c.value since it was folded before c was allocated.Under this interpretation we would expect a failure at line 8, since set does not require the necessary permissions.This also matches the semantics of ?, as de ned in [Wise et al. 2020], since the predicate instance folded at line 14 cannot consistently imply access to the heap location allocated at line 15.

Implementation
There are important implementation challenges that must be addressed before this change can be implemented in Gradual C0 [DiVincenzo et al. 2022].Currently, Gradual C0 constructs sets of permissions at run-time-before calling a method, for example-by recursively unfolding the neccessary speci cation and collecting all permissions.However, this method cannot be used to create the exclusion frame, since these permissions are not necessarily represented by a speci cation.But we expect that a translation algorithm can be developed which generates the source code necessary to compute the exclusion frame at run time.This is similar to the existing translation algorithm described by DiVincenzo et al. [2022], which translates symbolic run-time checks into source code that implements the desired assertion.
Also, note that we calculate the exclusion frame using information from symbolic execution of a particular statement.In other words, if method m calls m ′ , we can calculate the exclusion frame necessary for calling m ′ without considering the exclusion frame used to call m.This implies that exclusion frames can be dropped when entering a completely precise method, and then instantiated again when a precise method calls an imprecise method.This is similar to how Gradual C0 does not pass permission sets to precise methods, but reconstructs the permissions when a precise method calls an imprecise methods.Applying this technique to exclusion frames, as described, would ensure that exclusion frames do not a ect the run-time performance of methods that are speci ed with completely precise speci cations.

FUTURE WORK
There are many possible directions in which this work can be extended.We have not yet proven the gradual guarantees for gradual veri cation, as formalized in Wise et al. [2020].These guarantees formalize the notion that, given a valid program, gradual speci cations may be used in place of all static speci cations without introducing errors (both during veri cation and at run time).This ensures that any errors do not arise from imprecision, but rather from an invalid program or speci cation, or (for precise speci cations) from incompleteness of veri cation.Our formalization appears to satisfy this since, as described in §3, we extend the underlying static veri cation algorithm mainly by adding optimistic capabilities while leaving the bulk of static veri cation intact.However, we have not completed a formal proof.
Our formalization could also be used to extend gradual veri cation.Notably, gradual verication has not been implemented for quanti ed speci cations or concurrent programs.Ghost code/parameters (i.e., code only necessary for supporting logical proofs) is also not supported in gradual veri cation, since the "ghost" code could be necessary for run-time checks.Our high-level de nition of the gradual veri er could enable further development to support these techniques.Likewise, our formalization does not capture several important concepts in Viper such as domains, fractional permissions, and joining of symbolic execution paths.Formalizing the usage of these techniques in Viper and proving their soundness would provide further assurance of the correctness of Viper and provide a starting point for integrating these techniques with gradual veri cation.
Our formalization provides a basis for formally proving properties of veri cation techniques (in our case, gradual veri cation) with a model that closely resembles the implementation (in our case, Gradual C0).Thus modi cations to our formal model can be more easily implemented and used, while modi cations to the implementation can be re ected in the formal model and proven sound.

RELATED WORK
As mentioned previously, implementations of veri cation using symbolic execution, such as Viper [Schwerho 2016], Gradual C0 [DiVincenzo et al. 2022], Smallfoot [Berdine et al. 2005], Chalice [Leino et al. 2009], and jStar [Distefano and Parkinson 2008], often lack formal soundness proofs.A notable exception is VeriFast [Jacobs et al. 2011], which implements veri cation using symbolic execution.The core of its veri er was proven sound in Vogels et al. [2015].This soundness proof utilizes techniques from abstract interpretation, which may simplify proofs of veri ers using symbolic execution.However, VeriFast uses separation logic instead of IDF.
Several previous veri ers using WLP or veri cation condition generation (VCG) have been directly proven sound [Herms et al. 2012;Smans et al. 2012;Vogels et al. 2009Vogels et al. , 2010]].Several similar veri ers produce a proof during veri cation which may be checked to the validate soundness of an individual veri cation result [Filliâtre and Paskevich 2013;Parthasarathy et al. 2021].
Viper [Müller et al. 2016] andGradual C0 [DiVincenzo et al. 2022] rely on an SMT solver to implement their veri cation algorithms.While we have proved soundness of our formal model, this soundness is contingent on the soundness of the SMT solver.Other work has extended soundness to include soundness of the entire veri cation system.Notably, VeriSmall [Keuchel et al. 2022], Diaframe [Mulder et al. 2022], and Re nedC [Sammler et al. 2021] are all encoded in Iris/Coq, making them either foundational or self-verifying.
As described before, soundness of gradual veri cation based on WLP has been proven in both Wise et al. [2020] and Bader et al. [2018].However, Wise et al. [2020] depends on dynamically checking all assertions, while Bader et al. [2018] does not handle abstract heap predicates.

CONCLUSION
The recent implementation of gradual veri cation in DiVincenzo et al. [2022] promises a dramatic reduction in the e ort required to verify programs.However, this requires con dence in the correctness of their gradual veri cation system, Gradual C0, as well as its underlying static veri cation system, Viper.In this work, we formalized symbolic execution in (a subset of) Viper and proved it sound, in addition to formalizing gradual veri cation in Gradual C0 and proving it sound.During this work we found a soundness bug in Gradual C0, which we communicated to DiVincenzo et al.
[2022] along with possible solutions.This illustrates that, while correctness in gradual veri ers can be guaranteed, it should not be assumed without rigorous proof.There are a few interesting directions we could take this work: (1) proving that Gradual C0 adheres to the gradual guarantee as formalized by Wise et al. [2020], which is a very important property of gradual veri ers that should be straightforward to prove with our formal system, and (2) using our formalism to explore new directions in gradual veri cation like quanti cation or concurrency, and prove systems utilizing them sound.In general, we hope that this work serves as a strong basis for future proof work in static and gradual veri cation when using symbolic execution.

Fig. 12 .
Fig. 12. Rules for run-time check assertions [Reynolds 2002bolic expression composed of conjuncts identifying a particular execution path.Conjuncts are added at every conditional branch during symbolic execution.•Aeldchunk ⟨ , , ′ ⟩ ∈ SField represents, in the symbolic heap, the eld of an object reference containing a value ′ .A heap chunk is roughly approximate to the points to construct in separation logic[Reynolds 2002].A predicate chunk ⟨ , ⟩ ∈ SPredicate represents an isorecursive instance of a predicate with arguments .Together, eld chunks and predicate chunks are called heap chunks.• A symbolic heap H ∈ P (SField ∪ SPredicate) is a nite set of heap chunks.All heap chunks that it contains must represent distinct locations in the heap at run time.• A symbolic state ∈ SState is a tuple containing a path condition (referenced by ( )), a symbolic heap (referenced by H( )), and a symbolic environment (referenced by ( )).A symbolic state stores all values for a particular point during symbolic execution.The symbol empty represents an empty symbolic state, i.e. ( empty ) = true and H( empty ) = ( empty ) = ∅.• A veri cation state Σ represents a particular point during static veri cation.It is either a special