Demonstrating SPARK with a Mars Rover (Part 4): Formalizing Safety
What you'll learn:
- How to improve SPARK specifications.
- How safety can be specified in SPARK.
In this article, we delve more deeply into incorporating safety into the formal definition of the program. It takes a closer look at the initial implementation.
Demonstrating SPARK with a Mars Rover: 4-Part Series
- Cyber-Physical Systems
- The Safety Property
- Improving the System
- Formalizing Safety (this article)
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.
while not State.User_Exit loop
Go_Forward (State);
Find_New_Direction (State);
pragma Loop_Invariant (Rover.Cannot_Crash);
end loop;This loop doesn’t meet the precondition on “Go_Forward”. In the first iteration, it’s known only “Rover.Cannot_Crash” from the precondition on “Run”, which is weaker than “Rover_HAL.Get_Sonar_Distance >= Distance_Threshold”. And in subsequent iterations, it’s still known only “Rover.Cannot_Crash” because that’s all the loop invariant establishes.
Regardless of any other changes, the postcondition on “Find_New_Direction” must be strengthened to “Rover_HAL.Get_Sonar_Distance >= Distance_Threshold”. And since “Find_New_Direction” currently has no precondition (other than “Initialized”), it’s sensible to put it first in the loop:
while not State.User_Exit loop
Find_New_Direction (State);
Go_Forward (State);
pragma Loop_Invariant (Rover.Cannot_Crash);
end loop;
This way, the precondition on `Run` could be dropped altogether, allowing the Rover to start arbitrarily close to an obstacle and still avoid crashing.
Let’s see what it will take to get “Find_New_Direction” to promise “Rover_HAL.Get_Sonar_Distance >= Distance_Threshold”.
Improving “Find_New_Direction”
Here’s the current implementation of “Find_New_Direction”:
procedure Find_New_Direction (This : in out Auto_State)
with
Pre => Initialized,
Post => Initialized and then
Rover.Cannot_Crash
is
Left_Dist : Unsigned_32 := 0;
Right_Dist : Unsigned_32 := 0;
Timeout, Now : Time;
begin
Now := Clock;
Timeout := Now + Milliseconds (10000);
Set_Turn (Straight);
Set_Power (Left, 0);
Set_Power (Right, 0);
-- Turn the mast back and forth and log the detected distance for the left
-- and right side.
loop
Check_User_Input (This);
Now := Clock;
exit when This.User_Exit or else Now > Timeout;
Mast_Control.Next_Mast_Angle (This.Mast, -60, 70, 4);
if Mast_Control.Last_Angle (This.Mast) <= -40 then
Left_Dist := Sonar_Distance;
end if;
if Mast_Control.Last_Angle (This.Mast) >= 40 then
Right_Dist := Sonar_Distance;
end if;
Delay_Milliseconds (30);
end loop;
if Now > Timeout then
if Left_Dist < 50 and then Right_Dist < 50 then
-- Obstacles left and right, turn around to find a new direction
Turn_Around;
elsif Left_Dist > Right_Dist then
-- Turn left a little
Set_Turn (Around);
Set_Power (Left, -100);
Set_Power (Right, 100);
Delay_Milliseconds (800);
else
-- Turn right a little
Set_Turn (Around);
Set_Power (Left, 100);
Set_Power (Right, -100);
Delay_Milliseconds (800);
end if;
end if;
end Find_New_Direction;The logic here is to look to the left and the right and measure the distance to obstacles in both directions; then reorient the vehicle based on those measurements. If both left and right are too close (closer than 50 units), turn around. Otherwise, turn toward whichever direction has an obstacle further away.
This is intuitive, but it doesn’t guarantee that it will end up pointed in a direction with an obstacle more than 40 units — our distance threshold and the precondition for “Go_Forward” — away. For example, what if it doesn't turn far enough to the left or the right? The delay was chosen based on Rover dynamics. However, there’s no guarantee (in software!) that it will end up pointed in a direction with an obstacle more than 40 units away.
What if motor power drops during the turn and it doesn't turn as far as expected? Worse, what if the Rover finds itself inside of a (small) box with no exits? The current code would turn around and start moving forward without confirming that the way out is clear!
Now, it could be argued that these corner cases are impossible. But safety is about identifying corner cases and either removing them or arguing through risk assessment that the consequences are accepted if the corner case occurs (e.g., the Rover drives into a canyon on Mars and is blocked in by a rockfall).
Before the postcondition is strengthened to “Rover_HAL.Get_Sonar_Distance >= Distance_Threshold” and this subprogram updated to meet it, pause, and ask: “How is the current postcondition proved?” given the problems noted above.
The answer is, again, the safety property is (much) weaker than the new postcondition. Here, it’s met entirely because “Find_New_Direction” always leaves the Rover in a state in which it’s not moving straight forward. And that’s enough to satisfy the safety property, so the current postcondition proves. But it’s not enough to satisfy the precondition of our now-proved-safe “Go_Forward”.
Fortunately, fixing “Find_New_Direction” isn’t difficult. Here’s the updated, fully proved implementation:
procedure Find_New_Direction (This : in out Auto_State)
with
Pre => Initialized,
Post => Initialized and then
(This.User_Exit or else
Rover_HAL.Get_Sonar_Distance >= Distance_Threshold)
is
Left_Dist : Unsigned_32 := 0;
Right_Dist : Unsigned_32 := 0;
Timeout, Now : Time;
function Distance_Straight_Ahead return Unsigned_32 with
Side_Effects,
Pre => Initialized,
Post => Initialized and then
-- We have to make this promise because otherwise SPARK
-- cannot know that the result actually came from the
-- sonar.
Rover_HAL.Get_Sonar_Distance = Distance_Straight_Ahead'Result
is
begin
Rover_HAL.Set_Mast_Angle (0);
-- Set the mast to the straight position - this can take a bit of
-- time, so:
Delay_Milliseconds (50);
return Distance : Unsigned_32 do
Distance := Sonar_Distance;
end return;
-- Extended return used because SPARK won't allow a simple return
-- statement here, because the function has side effects.
end Distance_Straight_Ahead;
Distance : Unsigned_32;
begin
Now := Clock;
Timeout := Now + Milliseconds (10000);
Set_Turn (Straight);
Set_Power (Left, 0);
Set_Power (Right, 0);
-- Measure the distance straight ahead
Distance := Distance_Straight_Ahead;
while Distance < Distance_Threshold and not This.User_Exit loop
-- Turn the mast back and forth and log the detected distance for the
-- left and right side.
loop
Check_User_Input (This);
Now := Clock;
exit when This.User_Exit or else Now > Timeout;
Mast_Control.Next_Mast_Angle (This.Mast, -60, 70, 4);
if Mast_Control.Last_Angle (This.Mast) <= -40 then
Left_Dist := Sonar_Distance;
end if;
if Mast_Control.Last_Angle (This.Mast) >= 40 then
Right_Dist := Sonar_Distance;
end if;
Delay_Milliseconds (30);
end loop;
if Now > Timeout then
if Left_Dist < 50 and then Right_Dist < 50 then
-- Obstacles left and right, turn around to find a new direction
Turn_Around;
elsif Left_Dist > Right_Dist then
-- Turn left a little
Set_Turn (Around);
Set_Power (Left, -100);
Set_Power (Right, 100);
Delay_Milliseconds (800);
else
-- Turn right a little
Set_Turn (Around);
Set_Power (Left, 100);
Set_Power (Right, -100);
Delay_Milliseconds (800);
end if;
Distance := Distance_Straight_Ahead;
end if;
end loop;
end Find_New_Direction;The current functionality has been wrapped in a while loop that continues searching for a new direction until the distance is greater than or equal to the threshold of 40 units. A new local function was introduced to measure the distance straight ahead, as this behavior is required twice; encapsulation therefore keeps the implementation DRY (Don’t Repeat Yourself).
Notably, no loop invariants or assertions were required. SPARK proves that the revised postcondition holds based solely on the updated implementation.
Finalizing “Run”
There’s one more change needed to make to “Run”. Since “Find_New_Direction” promises “This.User_Exit” or “Rover_HAL.Get_Sonar_Distance >= Distance_Threshold”. It’s necessary to check for user exit before commanding forward motion:
while not State.User_Exit loop
Find_New_Direction (State);
exit when State.User_Exit;
Go_Forward (State);
pragma Loop_Invariant (Rover.Cannot_Crash);
end loop;And that’s it. The safety property throughout autonomous mode is fully proven, including while the Rover is driving forward in “Go_Forward”.
Conclusion: The Analysis of Our Demo
The Mars Rover demo shows how cyber-physical systems benefit from rigorous, provable software. Starting from SPARK Silver (absence of run-time errors), the team introduced a meaningful Gold-level safety property — “the rover must not proceed straight when an obstacle is within the safety threshold” — and encoded it through a clean HAL model with Ghost getters, a composable safety monitor, and targeted contracts.
In doing so, the exercise surfaced and removed a latent assumption in remote-control mode and then strengthened autonomous mode with a simple dynamics model and modularized assumptions. This enabled full proofs of Go_Forward and Find_New_Direction with minimal auxiliary annotations.
Beyond the specific rover, this work illustrates a repeatable pattern for CPS: isolate hardware effects, express intent as contracts, attach proofs where they matter, and let the analysis expose unsafe edge cases before they reach hardware. The result is not only higher assurance, but clearer software architecture and more maintainable code.
End of Demonstrating SPARK with a Mars Rover
This was part 4 of the series:
- Cyber-Physical Systems
- The Safety Property
- Improving the System
- Formalizing Safety (this article)
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:




