Demonstrating SPARK with a Mars Rover (Part 3): Improving the System
What you'll learn:
- How to use pre/post and loop invariant conditions.
- How SPARK helps provide that this cyber-physical system will operate as specified.
This is Part 3 of the four-part series on how the SPARK programming language was used to manage and control a model Mars Rover robot. This article looks at the improvements using Ada’s contract support in SPARK to improve the support for this cyber-physical system.
Demonstrating SPARK with a Mars Rover: 4-Part Series
- Cyber-Physical Systems
- The Safety Property
- Improving the System (this article)
- Formalizing Safety
One of the goals for this Mars Rover Demo project was to demonstrate how SPARK can address safety and reliability issues. You can download the sources for the Mars Rover Demo project.
Using Cyber-Physical Systems (CPS)
Cyber-physical systems (CPS) are integrated solutions that combine computation with physical processes, blurring the boundaries between the digital and physical worlds. They rely on tight interaction between software, networking, and mechanical or electronic components, often using sensors, actuators, and embedded systems to perceive, analyze, and influence their environment. Examples span a wide range of industries, including smart grids, autonomous vehicles, medical monitoring, industrial control systems, robotics, and avionics.
Because they coordinate sensing, computation, and actuation, CPS is an ideal framework for understanding the challenges and opportunities in high-integrity embedded development.
Our new Ada Mars Rover demo platform is a hands-on example of a CPS in action. Like real-world rovers, it integrates embedded software with hardware components to sense its surroundings, process data, and act accordingly — all in real-time. Beyond showcasing the principles of CPS, the project is designed to highlight the strengths of the SPARK language and tools for developing high-integrity software in safety- and mission-critical domains.
Stage 2: Improving the System
Following this initial work, the team was inspired to take another look at what he was proving. In this section, Tony Aiello takes things a step further.
Tony was increasingly unhappy with this loop, in “Autonomous_Mode.Run”:
while not State.User_Exit loop
Go_Forward (State);
Find_New_Direction (State);
pragma Loop_Invariant (Rover.Cannot_Crash);
end loop;
The loop invariant states that the Rover.Cannot_Crash, so we know that the Rover isn’t in an unsafe state _at that point_ — but the team didn't like their failure to assert the safety property after “Go_Forward” (“Rover.Cannot_Crash” isn’t a postcondition of “Go_Forward” here). They proved that, after finding a new direction, the Rover would be in a safe state. However, they didn’t prove that after going forward, the Rover would be in a safe state.
This was addressed in stage one by making an informal argument about the vehicle dynamics and its impact on the correctness of the code:
There are two conditions under which we exit [the “Go_Forward”] loop: (1) if the user presses a button; and (2) if the distance to an obstacle is less than 30 units (which is more conservative than our safety threshold). Unfortunately, neither of these conditions on its own is enough to say that it’s safe. For (1), the Rover could be too close to an obstacle (and still be commanded to move straight forward). For (2), the upper bound on the distance is known, but not the lower bound — you would have to know that the distance is, e.g., greater than or equal to 20 but less than 30. Vehicle dynamics and the update rate of the sonar may guarantee this, but this information isn’t available simply from the code.
Now, this is a valid approach to dealing with code that depends on real-world assumptions which can’t be checked in software. But it felt unsatisfying for this demo. In this section, the Rover demo is updated to formalize the informal argument made in the prior section.
The result is a more satisfying autonomous mode that does a better job of guaranteeing that the Rover can’t crash.
Proving “Go_Forward”
Here’s the complete “Go_Forward” subprogram from before (with a couple of the magic numbers replaced by constants):
Distance_Threshold : constant := 40;
Sonar_Sampling_Delay : constant := 40;
procedure Go_Forward (This : in out Auto_State) with
Pre => Initialized and then
Rover.Cannot_Crash,
Post => Initialized
is
Distance : Unsigned_32;
begin
-- Go forward...
Set_Turn (Straight);
Set_Power (Left, 100);
Set_Power (Right, 100);
-- Rotate the mast and check for obstacle
loop
Check_User_Input (This);
exit when This.User_Exit;
Mast_Control.Next_Mast_Angle (This.Mast, -60, 70, 16);
Distance := Sonar_Distance;
exit when Distance < Distance_Threshold;
Delay_Milliseconds (Sonar_Sampling_Delay);
end loop;
end Go_Forward;It would be great to prove that the Rover is safe during the loop and when the subprogram returns. “Rover.Cannot_Crash” should be added to the postcondition and as a loop invariant. The loop invariant should be placed after the second exit statement, like this:
exit when Distance < Distance_Threshold;
pragma Loop_Invariant (Rover.Cannot_Crash);
Delay_Milliseconds (Sonar_Sampling_Delay);
end loop;SPARK will prove the loop invariant: At that point in the loop, it’s assured that Distance is greater than 40, which is greater than our safety threshold of 20.
However, SPARK can’t prove the postcondition. Execution may exit when the measured distance falls below 40, but, as noted in the previous section, there’s no guarantee that the Rover remains above the safety threshold of 20. The Rover could be moving quickly enough for the distance to drop from, for example, 41 to 19 in a single loop iteration. Additional information is therefore required.
Unfortunately, the additional information needed doesn’t exist anywhere in the software. Instead, this extra information is a physical property of the system — the Rover simply isn’t able to move fast enough on its own so that it could close the difference between the distance and safety thresholds in 40 ms. The design of software depends on this fact, which needs to be communicated to SPARK.
One option is to use an assumption. An assumption — unlike an assertion — will not be proved by SPARK. SPARK will simply use the assumption to help prove subsequent checks. The assumption can be added like this:
pragma Assume (Distance >= Rover.Safety_Distance);
exit when Distance < Distance_Threshold;
pragma Loop_Invariant (Rover.Cannot_Crash);
Delay_Milliseconds (Sonar_Sampling_Delay);
end loop;But… this still doesn’t prove! Why not?
Two exit statements are in the loop.
There can be confidence that the postcondition at the second exit statement has been addressed, because of the new assumption. But the first exit statement remains unaddressed. (This can be verified by asserting “Rover.Cannot_Crash” just before the first exit statement; SPARK will fail to prove this assertion.)
To do this, the precondition on “Go_Forward” needs to be strengthened. The Rover must be in a safe state, but “Rover.Cannot_Crash” is too weak to enable us to prove “Rover.Cannot_Crash” as our postcondition at the first exit, the first time around the loop. Why? Because “Rover.Cannot_Crash” is true if the sonar distance is greater than the safety threshold, OR it’s not moving straight forward.
Therefore, the precondition holds if, e.g., it’s moving backwards and the sonar distance is 5. But then the first thing “Go_Forward” does is start moving forward. If it took the first exit, it wouldn’t be safe anymore. (Moreover, our assumption would definitely be invalid in this case!)
So, the precondition should replace “Rover.Cannot_Crash” with the stronger expression “Rover_HAL.Get_Sonar_Distance >= Rover.Safety_Distance”.
Now SPARK can fully prove “Go_Forward”, given the assumption that was added.
In safety-critical systems, it’s common for a safety property (or a safety invariant) that’s proven to hold _now_ to be insufficient (too weak) to guarantee that it will hold _later_, e.g., during the next iteration of a loop. What’s required in this case is a stronger property, often referred to as an inductive invariant, that _is_ strong enough to guarantee the safety property will hold in a future state. (And, indeed, we typically need to know that the inductive invariant will guarantee that it holds in a future state, and we can keep running safely.) As we’ll see in the next section, the inductive invariant needed here is that the distance to an obstacle is greater than or equal to 40 units — a much stronger property than the 20-unit safety threshold that’s part of our safety property!
Relaxing the Assumption
While this works, I don’t like it. As we just saw, assumptions are a bit dangerous. Since SPARK can’t check them, it’s far too easy to assume something that just isn’t valid. (It’s also sometimes easy to assume something that’s trivially false, which has disastrous implications for the remainder of the analysis, since in logic False → False is True.)—Tony Aiello, AdaCore
First, assuming “Distance >= Rover.Safety_Distance” outright is far too strong. If an error were made in defining “Sonar_Sampling_Delay” and set to 400 ms or 4000 ms, would the Rover still be safe? There’s an implicit understanding of the Rover’s dynamics that informed the belief the code was correct; instead, that understanding could be encoded explicitly.
The Rover must have a maximum speed that it can achieve. Let’s define “Max_Speed” to be 0.5 units per millisecond. It’s actually been measured to be 0.012 units per millisecond. In the final code, we set “Max_Speed” to 0.015 for a bit of additional margin — we don’t need to squeeze all of the possible performance out of the system
It then becomes possible to compute the maximum distance the Rover could travel within a given number of milliseconds and use this in the assumption instead. By recording the last distance measurement, one can state: “Distance >= Last_Distance - Sonar_Sampling_Delay * Max_Speed”.
Taking types into account, the assumption can be rewritten as:
pragma Assume
(Distance >=
(declare
Displacement renames
Unsigned_32 (Max_Speed * Float (Sonar_Sampling_Delay));
begin
(if Last_Distance < Displacement then
0
else
Last_Distance - Displacement)));To do this, it’s necessary to introduce “Last_Distance”, declared as “Ghost” (since it’s required solely for proof). An initial value for “Distance” is also needed, as it will be read (to assign to “Last_Distance”) prior to polling the sonar. Initialization should set “Distance” to “Rover.Safety_Distance”, as specified in the precondition.
When the proof is repeated, it fails: SPARK can’t prove the postcondition. If “Rover.Cannot_Crash” is asserted before the second exit statement, it becomes evident that this is the path along which SPARK is unable to prove the postcondition. The assertion can’t be proved, but SPARK does prove the postcondition under the assumption that the assertion holds.
pragma Assume
(Distance >=
(declare
Displacement renames
Unsigned_32 (Max_Speed * Float (Sonar_Sampling_Delay));
begin
(if Last_Distance < Displacement then
0
else
Last_Distance - Displacement)));
pragma Assert (Rover.Cannot_Crash); -- This is not proved
exit when Distance < Distance_Threshold;Intuitively, the failure to prove the assertion indicates that “Last_Distance” is the problem: SPARK is told that the distance to the obstacle is greater than a function of “Last_Distance”, but SPARK’s clearly not seeing that this relationship is as strong as needed.
The intuition can be confirmed by strengthening the assertion to “Distance >= Rover.Safety_Distance”, which SPARK can’t prove. Further confirmation that the problem is “Last_Distance” is accomplished by adding an assertion about “Last_Distance” in front of the assumption.
pragma Assert (Last_Distance >= Distance_Threshold); -- Not proved
pragma Assume
(Distance >=
(declare
Displacement renames
Unsigned_32 (Max_Speed * Float (Sonar_Sampling_Delay));
begin
(if Last_Distance < Displacement then
0
else
Last_Distance - Displacement)));
exit when Distance < Distance_Threshold;You need to know that “Last_Distance >= Distance_Threshold” for the assumption to establish that “Distance >= Rover.Safety_Distance”; if asserted, SPARK can’t prove it. Three changes are needed to fix it.
First, because “Last_Distance” is assigned the value of “Distance” that was constrained in the _prior_ iteration of the loop, the constraint on “Distance” must be expressed as a loop invariant. (Remember that, in SPARK, loops represent cut points in the analysis for any variables assigned during the loop. Only type properties and properties established in loop invariants are carried from one loop iteration to the next.)
Accordingly, “Distance >= Distance_Threshold” is added as a new loop invariant, next to “Rover.Cannot_Crash”. This ensures that SPARK will know that “Last_Distance >= Safety_Threshold” on subsequent loop iterations.
But what about the first iteration? This leads to the second and third changes. The precondition must be strengthened from “Rover_HAL.Get_Sonar_Distance >= Rover.Safety_Distance” to “Rover.Get_Sonar_Distance >= Distance_Threshold” and the initialization of “Distance” must be updated to match. It will ensure that, on the first iteration, “Last_Distance >= Safety_Threshold”.
procedure Go_Forward (This : in out Auto_State) with
Pre => Initialized and then
Rover_HAL.Get_Sonar_Distance >= Distance_Threshold,
Post => Initialized and then
Rover.Cannot_Crash
is
Distance : Unsigned_32 := Distance_Threshold;With these changes, when SPARK is rerun, the postcondition is proved.
It would be possible to stop at this point, but retaining this assumption in the code remains undesirable. The presence of a validation-worthy assumption isn’t immediately evident, and reusing this dynamics model elsewhere would require repetition.
Fortunately, SPARK has the tools to modularize these kinds of assumptions.
Modularizing the Assumption
The assumption is modularized in the same way that we might introduce a lemma or describe the contract on a binding to another language: Declare a procedure whose postcondition is the assumption and mark it Ghost and Import.
procedure Rover_Displacement_Model
(Distance : Unsigned_32;
Last_Distance : Unsigned_32;
Ellapsed_Time : Unsigned_32)
with
Global => null,
Post => (Distance >=
(declare
Displacement renames
Unsigned_32 (Max_Speed * Float (Ellapsed_Time));
begin
(if Last_Distance < Displacement then
0
else
Last_Distance - Displacement))),
Ghost,
Import,
Always_Terminates;Since SPARK’s analysis is modular, when this procedure is called, SPARK will assume the postcondition whenever the precondition (which is empty here) is met. The assumption in “Go_Forward” can now be replaced with a “call” of this Ghost procedure.
At least two implicit assumptions underpin the correctness of invoking our dynamic model here: (1) the sonar measurements overlap the axis of motion (i.e., detect obstacles straight forward) and (2) the elapsed time since the last measurement is no greater than the sonar sampling delay. We could encode these as preconditions and prove they hold at the point of call. But to do so, we’d have to modify our HAL to report the direction and time of the sonar measurement so that we can retrieve these via (models of) getters. For the moment, we’ll leave this enhancement as an exercise for the reader.
Here’s the final form of “Go_Forward” that incorporates all of the changes and is fully proved:
Distance_Threshold : constant := 40;
Sonar_Sampling_Delay : constant := 40;
procedure Go_Forward (This : in out Auto_State) with
Pre => Initialized and then
Rover_HAL.Get_Sonar_Distance >= Distance_Threshold,
Post => Initialized and then
Rover.Cannot_Crash
is
Distance : Unsigned_32 := Distance_Threshold;
Last_Distance : Unsigned_32 with Ghost;
begin
-- Go forward...
Set_Turn (Straight);
Set_Power (Left, 100);
Set_Power (Right, 100);
-- Rotate the mast and check for obstacle
loop
Check_User_Input (This);
exit when This.User_Exit;
Mast_Control.Next_Mast_Angle (This.Mast, -60, 70, 16);
Last_Distance := Distance;
Distance := Sonar_Distance;
Rover_Displacement_Model
(Distance, Last_Distance, Sonar_Sampling_Delay);
-- We invoke the Rover displacement model so that SPARK knows the
-- limits on how far the rover can have traveled since the last
-- distance measurement.
exit when Distance < Distance_Threshold;
pragma Loop_Invariant (Distance >= Distance_Threshold);
pragma Loop_Invariant (Rover.Cannot_Crash);
Delay_Milliseconds (Sonar_Sampling_Delay);
end loop;
end Go_Forward;Demonstrating SPARK with a Mars Rover 4-Part Series
This was part 3 of the series.
- Cyber-Physical Systems
- The Safety Property
- Improving the System (this article)
- Formalizing Safety
More About Ada and SPARK
About the Author

Tony Aiello
Product Manager, AdaCore
Tony Aiello is a Product Manager at AdaCore. Currently, he manages SPARK Pro and GNAT Pro for Rust, with a hand in UX and application of AI to AdaCore’s products. Tony has been principal investigator of multiple United States Air Force research projects that focus on the enhancement and application of formal methods to Air Force-relevant applications.
Previously, Tony led the SSI and QGen teams, led research directions, and was Head of Product at AdaCore. Before joining AdaCore, Tony was a Principal Scientist at, and briefly President of, Dependable Computing, a small research and development group focused on safety and formal methods.
Comment About the Article
To join the conversation, and become an exclusive member of Electronic Design, create an account today!

Leaders relevant to this article:




