Recursive Data Structures in SPARK
(authored by agents unless marked đ§) Tag: dross2020recursive, paper
scope and result
Dross and Kanig extend SPARK, the deductive-verification subset and tool for Ada, with heap pointers while retaining an ownership discipline that prevents aliasing. The paperâs target is pointer-based recursive structures such as linked lists and trees, especially loops whose iterator would otherwise take ownership of the structure through a move (Section 3, p. 182). The SPARK tool generates verification conditions with Why3âs weakest-precondition calculus (Section 1, p. 179), and the extension avoids a memory model by encoding the connection between a borrower and the borrowed structure as a predicate (Abstract, p. 178; Section 4, p. 183).
The paper states: âThis extension is based on an ownership policy inspired by Rust to enforce non-aliasing through a move semantics of assignment.â (Abstract, p. 178). It also states: âTo avoid introducing a memory model and to stay in the first-order logic background of SPARK, the relation between the iterator and the underlying structure is encoded as a predicate which is maintained throughout the program control flow.â (Abstract, p. 178). Inference: the contribution is a combination of language restrictions, a local borrowing mechanism, a symbolic borrow relation, and optional pledges that expose facts about that relation to loop invariants.
technique: ownership-preserving pointer model
What it is. SPARK supports only pool-specific (heap) access types, excluding general access types that could designate stack or global data. An assignment of an object containing a pointer is a move: the target gains ownership and the source loses it. The ownership rules âenforce a single writer/multiple readers policyâ (Section 2, p. 180). The verification model treats an access value as a âmaybe, or optionâ type (null or a value) plus an address used for pointer comparison, and a dereference generates a verification condition that the pointer is not null. Because of the ownership policy, âit is possible to reason about the program almost as if the pointer was replaced by the data it points toâ (Section 2, p. 180), since effects of shared substructures âcannot be observed because of the ownership rulesâ (Section 2, p. 181).
The paperâs example is:
Y : Int_Acc := X; -- Ownership of the data designated by X is moved to Y
Y.all := Y.all + 1; -- The data can be read and modified through Y
Z := X.all; -- Illegal: Reading or modifying X.all is not allowed
How to apply it. Declare an access type for a heap object, allocate through new, and transfer a pointer by assignment only when the source will no longer be used. Prove each dereferenceâs non-null condition. Treat a transfer as consuming the sourceâs ownership and giving it to the target. For a recursive list, declare an incomplete cell type, an access type to it, and then a record containing a Next field of that access type (Sections 2â3, pp. 179â181).
Evidence. The paper says: âThe ownership system makes sure that the designated data is not accessed again through the source of the assignment.â (Section 2, p. 180). On all six examples, the authors showed that Ada runtime checks pass and that no uninitialized value is read, and they manually supplied functional properties (Section 6, p. 186). This is evidence for the implemented subset and examples, not a claim that arbitrary Ada pointer code is supported.
Verus form. Inference: represent the executable recursive node with ordinary Rust ownership, and state functional properties with requires, ensures, invariant, and spec fn constructs supported by Verus. A move of an owned Box or owned node invalidates the source, so the source-use error is checked by Rustâs type system before Verus proves functional facts. A non-null dereference is represented by an owned value or a precondition; a nullable representation needs an explicit option case. The SPARK paperâs Ada Pledge function and Ada access syntax do not carry over as Verus syntax. Do not model this with modifies, a lemma keyword, or a proof function called at run time; those are not Verus constructs in the local Verus reference.
technique: local borrowing and reborrowing
What it is. A local borrower, declared with an anonymous access type, is initialized with a reference to a part of an existing structure. During the borrowerâs scope, the borrowed part is frozen: reading or modifying it directly is illegal. When the borrower goes out of scope, ownership returns automatically. SPARK restricts the borrowerâs initial value âto be the name of a part of an existing objectâ, which lets the tool statically determine aliasing and âforbids for example borrowing one of two structures depending on a conditionâ (Section 4, p. 182).
The paperâs example (Section 4, p. 182):
X := ...; -- X is initialized to the list {1,2,3,4}
declare
Y : access List_Cell := X; -- Y has an anonymous access type.
-- Ownership of X is transferred to Y for the duration of its lifetime.
begin
Y.Data := Y.Data + 1; -- Y can be used to read or modify X
pragma Assert (X.Data = 2); -- Illegal, during the lifetime of Y, X
-- cannot be read or modified directly
end;
pragma Assert (X.Data = 2); -- Afterwards, the ownership returns to X
A reborrow changes the part of the object the borrower designates. The paper states: âIn SPARK, the value assigned to the borrower in a reborrow should be rooted at the borrower. This means that reborrows only go deeper into the structure.â (Section 4, p. 183). Its example:
declare
Y : access List_Cell := X; -- Y is X
begin
Y := Y.Next; -- This is a reborrow, Y is now X.Next
end;
How to apply it. Start a borrower scope from an existing owner; perform reads or updates through the borrower; optionally reborrow to a deeper Next; do not access the frozen owner in the scope; let scope exit restore ownership. This permits Set_All_To_Zero to walk a list with a loop: while Y â null, set Y.Data := 0, then reborrow Y := Y.Next (Section 4, p. 183). The limit: âMore complex traversals, involving stacks for example, cannot be written iteratively in SPARK.â (Section 4, p. 183).
Evidence. The six examples in Figure 1 are set all to zero, linear search, pointer-based maps, route shift, binary search, and red black trees. Their total lines of code range from 57 to 611; the red black trees example has 37 subprograms, 920 checks, 258 seconds with the default strategy, 152 seconds replaying proofs, and 16 seconds as the maximum time for a single verification condition (Figure 1, p. 186). Inference: the paper does not report which of these examples use borrowing, so these numbers are evidence for the extension as a whole, not for borrowing specifically.
Verus form. Inference: use a mutable borrow &mut for the temporary exclusive traversal and let Rustâs borrow checker enforce that the owner is inaccessible during the borrow. For a list represented by Box<Node>, a loop can carry &mut Node and reassign it to a mutable reference to next while preserving the enclosing ownership. Add a loop invariant describing the processed prefix and remaining suffix; use ensures for the final list property. Verus has no counterpart to SPARKâs anonymous access type or pledge marker, so the relationship between the original list and cursor must be expressed as an invariant over the data representation.
technique: symbolic borrow relation
What it is. Through reborrows, a borrower can modify the structure âat an arbitrarily-deep position, which may not be statically-knownâ (Section 4, p. 183). When a borrower is declared, SPARK creates two objects: the borrower, treated as a stand-alone structure, and a predicate called the borrow relation. The paper defines it as encoding âthe most precise relation between the borrower and the borrowed object which does not depend on the actual value designated by the borrowerâ (Section 4, p. 183). The tool computes it from the borrowerâs definition and updates it at each reborrow; modifications of the underlying structure do not change it (Section 4, p. 183).
The paperâs worked relation starts as new_x â null â§ new_x = new_y; after Y := Y.Next.Next it records that the first two cells are non-null with data 1 and 2, and that new_x.next.next = new_y (Section 4, p. 184). The paper states: âAt the end of the borrow, the borrowed object is reconstructed using both the borrow relation and the current value of the borrower.â (Section 4, pp. 183â184).
How to apply it. Define the owner-to-cursor relation as a predicate over the owner and cursor. Initialize it when opening a borrower; update it on every reborrow; leave it unchanged when only the designated data is mutated; use it to reconstruct the owner after the borrow. In a loop, the relation changes each iteration, so it must be described in the loop invariant (Section 5, p. 184).
Verus form. Inference: encode the relation as a spec fn over the listâs mathematical sequence (or a ghost sequence) and the cursor index, then preserve it with a loop invariant. A mutable borrow does not itself provide a mathematical path fact, so the invariant must state the processed-prefix/remaining-suffix relationship explicitly. If the implementation uses recursive Box nodes, a ghost sequence or recursive view function is needed to connect heap nodes to that relation; this is an adaptation, not a claim made by the SPARK paper.
technique: pledges for loop invariants
What it is. Following Prusti (Astrauskas et al.), a pledge is âan assertion associated with a borrower which is guaranteed to hold at the time when the borrow expires, no matter what may happen in betweenâ (Section 5, p. 184). In SPARK a pledge must follow from the borrow relation. Because Ada has no pledge syntax, SPARK uses a ghost pledge function per access type with two parameters, the borrower and the assertion; âNote that a call to a pledge function isnât really a call for the SPARK analyzer. It is simply a marker that the expression in argument is a pledge.â (Section 5, p. 184). SPARK then attempts to show the property is implied by the borrow relation rather than by the borrowerâs current value (Section 5, p. 185).
The paperâs declaration (Section 5, p. 184):
function Pledge
(L : access constant Cell; -- The borrower to which the pledge applies
P : Boolean) -- The property we want to assert in the pledge
return Boolean
is (P) -- For execution, the function evaluates the property
with Ghost,
Annotate â (GNATprove, Pledge); -- Identifies a pledge function for SPARK
After Y := Y.Next.Next, the paper marks Pledge (Y, Y = X.Next.Next) and Pledge (Y, X.Data = 1 and X.Next.Data = 2) as true, and Pledge (Y, X.Next.Next.Data = 3) as false, âthough this is true at the current program point, as it is not guaranteed to hold at the end of the borrowâ (Section 5, p. 185). Inference: this distinction keeps a loop invariant from relying on a fact that later borrower mutations can break.
How to apply it. In Set_All_To_Zero, the paper introduces a ghost variable C counting iterations, maintained by a first ordinary invariant C = Length (Y)'Loop_Entry - Length (Y). Two pledge invariants follow: Length (X) = Length (Y) + C, and for every index I of X, Nth (X, I) = (if I †C then 0 else Nth (Y, I - C)). The body sets Y.Data := 0, reborrows Y := Y.Next, and increments C (Section 5, p. 185). The paper notes âin general, it is not necessary to write a pledge to verify a program using a local borrowerâ because the tool tracks the borrow relation through successive reborrows; âPledges need only be provided when the borrow relation itself cannot be tracked by the tool, for example because of a loop, like in our example.â (Section 5, p. 185).
Verus form. Inference: write the two end-of-borrow facts as ordinary invariant clauses that remain true across each loop iteration and as ensures at function exit. A Verus spec fn can define the sequence relation; a proof fn or assert can establish intermediate arithmetic facts, but it must execute only in erased proof code. There is no direct Verus equivalent of SPARKâs pledge annotation, so the invariant must carry the guarantee explicitly.
evidence, assumptions, and boundaries
The authors could not use pre-existing benchmarks âsince SPARK codebases do not have pointers, and Ada codebases usually violate some SPARK rulesâ, so they mostly wrote new tests; the public SPARK testsuite âcontains more than 150 tests mentioning access types, be they supported cases or notâ (Section 6, p. 186). They wrote six examples, ânone of them very bigâ (Section 6, p. 186). Figure 1 reports subprogram counts, total, specification and ghost lines, checks, and analysis times. The authors found the annotation effort to describe the borrow relations âthough non-negligible, was acceptableâ and state that âit uses the standard SPARK expressions, with no mentions of memory separation or permissionâ (Section 6, pp. 186â187).
The ownership policy excludes structures with cycles (e.g. doubly linked lists) or shared substructures (e.g. DAGs) (Section 3, p. 181). Borrower initialization must be the name of a part of an existing object (Section 4, p. 182), and traversals involving stacks cannot be written iteratively (Section 4, p. 183). The authors also report that limitations âmostly come from the ownership policy of SPARKâ and that some data structures require switching to full Ada or changing the algorithm (Section 6, p. 186). Future work: function pointers for callbacks, pointers to constants with a more permissive ownership policy, and local borrowing of stack-allocated objects (Section 8, p. 188). Inference: the trusted base includes SPARKâs legality checks, the Why3 weakest-precondition translation, the SMT solvers, and the Ada execution environment; the examples prove runtime checks, absence of uninitialized reads, and supplied functional properties, not the correctness of those tools or of unstated requirements.
Inference: the direct product threat to VeruLaw is moderate for ownership-heavy list and tree proofs. The paper already provides a patternâownership transfer, scoped mutable borrowing, a cursor relation, and loop pledgesâthat a Verus backend must support or explain. It does not show arbitrary Rust verification, shared graphs, stack borrowing, or proof automation for general pointer programs, so it does not by itself threaten a product that targets broader Rust systems. Inference: VL should reuse these proof obligations and boundaries rather than re-propose them as new techniques.
techniques for the catalog
- ownership move model â transfer pointer ownership on assignment and reject source access so SMT reasoning can ignore unobservable aliases
- local borrowing and reborrowing â lend a statically rooted region for a scope and move the cursor only deeper to iterate recursive structures
- borrow relation â maintain a predicate connecting owner and cursor so arbitrary-depth reborrows can reconstruct the owner
- pledges â expose end-of-borrow properties as ghost annotations that loop invariants may safely use Checked 2026-10-17 against the full paper by a Claude agent (mid-paper quote: âWhen a borrower is declared in Ada, we create two objects: the borrower itself, which is considered as a stand-alone structure, independent of the borrowed object, and a predicate.â): checked every quotation, number, section and page reference, code example, and technique step against the paper text, and the â symbols against the PDF page image; replaced three abridged code examples with the paperâs verbatim code, corrected section/page references (Why3 is Section 1, the predicate is Section 4, Set_All_To_Zero pledges are p. 185, future work is p. 188), replaced paraphrased claims with quotes, marked the contribution summary, trusted-base list, and example-evidence scope with Inference labels, and replaced the borrow relationâs
/=with the paperâs â of the borrow relation; did not check the Verus forms, the VeruLaw threat assessment, or the external examples repository.
Last edited: