RR's Ramblings

Checking Code Quality Warnings: Highlighting More Potential Bugs
June 25, 2019
by Randy Brukardt

Previously, I had described the intent and details of the new Quality Warnings found in Janus/Ada 3.2.1 (see Code Quality Warnings: Highlighting Likely Bugs before They Bite). The original set of warnings did not include the access value patterns suggested in the motivating AdaCore blog item. These were omitted as they could not be easily implemented with the information available in the Janus/Ada front-end.

Specifically, one of the patterns looks like:

   if P = null then
      ... P.all ...
   end if;

In this code, the null access value check for the dereference of P will always fail and raise Constraint_Error. Unless this is an ACATS or similar test, this is a bug.

The Janus/Ada compiler generally processes a single expression at a time. This is in part a result of its heritage as a small host compiler – processing one expression at a time uses less memory than keeping larger structures in memory.

However, the access value pattern given above contains two separate expressions, and they can be separated by a long distance (an arbitrary number of other expressions and nesting could be between them). This makes it impractical to detect a pattern like this in the Janus/Ada front-end outside of the most trivial cases.

The Janus/Ada optimizer usually has access to larger portions of code. Moreover, the optimizer can check most causes of this sort of problem, rather than just a specific pattern, giving broader coverage for the warning. It also already supports many of the operations needed to determine if this problem is truely present, reducing the number of false positives. Therefore, we looked at supporting this warning within the optimizer. One problem with doing so is that the optimizer was not set up to report warnings or errors. Location information in the intermediate code generally only identifies the starting line of an expression; more precision is clearly needed for an understandable quality warning (especially as it often will be reporting on a check associated with an implicit dereference or a subtype conversion not visible in the source code). This problem caused us to leave these warnings out of our initial set of Quality Warnings.

Quality Code will typically be written such that language-defined checks cannot fail. For instance, for access types, either null excluding subtypes or explicit checks will be used to ensure that an access value cannot be null. Such code will be a lot more common than quality bugs. Building the quality check into the optimizer also adds the possibility of eliminating the code for checks that can be proven to be unneeded. This can be even more valuable than the quality warnings, as it improves the resulting code and makes it less necessary to suppress checking.

Further thought on this topic demonstrates that these points apply to almost all language-defined checks. One would often want to protect discriminant checks, index checks, even assertion checks with either explicit code or subtypes. And if that is done, analysis of the code can possibly remove the checks by proving they cannot fail, prove a likely bug, or neither.

Therefore, we determined to create a second set of Quality Warnings, the Checking Quality Warnings. These are implemented in the optimizer, in part by associating detailed location information with each language-defined check. We started the implementation with access checks, as these have the fewest possible states to account for. This let us use access checks to prove the concepts. For each associated check, the optimizer determines if the check cannot fail, must fail, cannot be executed, or that it cannot determine any of these. All of these results except the first are associated with a warning, using the provided warning location to precisely identify the check.

The following function illustrates all of these cases:

   type My_Access is access Natural;

   function Get (P : My_Access) return Natural is
      T : Natural;
   begin
      T := P.all; -- (1)
      if P /= null then
          T := P.all; -- (2)
      elsif P = null then 
          T := P.all; -- (3)
      else
          T := P.all; -- (4)
      end if;
      return T;
   end Get;

(1) is the case where the optimizer cannot tell whether or not the access check will be triggered. This will generate a warning that a "null check may fail". (2) is the correct quality code, and no warning will be generated nor will any check code be generated. (3) is the case where the check will always fail. A warning that the check will always fail will be generated. This is a bug of some sort – the condition could be wrong, the code could be in the wrong if branch, or there may be some other problem.

Finally, (4) is the case where the check cannot be executed. A warning about unreachable code is generated (as opposed to the usual informational message). We consider this case more severe than the usual unreachable code, as it cannot be caused by intentional conditional compilation.

These warnings are implemented in Janus/Ada 3.2.1. These work well, other than that for the "null check may fail" warning is sometimes reported when it is obvious that it should not be (a "false positive").

The most common case of a false positive is when a subprogram parameter is assumed to not be null, but this is not declared in the code using a null exclusion. This is easily fixed by adding a null exclusion (not null) directly to the declaration of the formal parameter (a separate subtype is not needed). The check will then be done at the site of the call, which should be easier to verify. Of course, the same protections need to be applied to the actual parameter(s) in that case.

A similar case occurs for temporary objects which are assumed to be not null. This is also easily fixed by adding a null exclusion (not null) directly to the declaration of the temporary object. The check will then be done when the temporary object is assigned.

Implementing these warning using the optimizer means of course that the warnings are limited by the limitations of the optimizer. This causes additional forms of false positive. In most of these cases, some restructuring will eliminate the issues.

For instance, the optimizer will not analyze code across exception handler boundaries, in order to properly implement 11.6 rules. That means that in an example like:

   if P /= null then
       begin
           T := P.all; -- (5)
       exception
           when Constraint_Error => null;
       end;
   end if;

Even though the test and use are close together, they are separated by an exception handler. Ada limits the motion of code across handlers, and Janus/Ada handles this by optimizing the code before, during, and after the exception handler applies separately. This causes a false positive at (5). The solution for a case like this is to introduce a null excluding object to move the check outside of the handler:

   if P /= null then
       declare
           Temp_P : not null My_Access := P;  
       begin
           T := Temp_P.all;
       exception
           when Constraint_Error => null;
       end;
   end if;

The null check is now outside of the exception handler and now should visible to the optimizer along with the condition. The dereference does not need a null check since the subtype excludes null, so no "may fail" warning will occur.

If there is an individual check whose warning just cannot be eliminated, but can be determined to be safe by other means, pragma Warning_Level can be used to eliminate the warning in a small area. Warning_Level works much like Suppress, setting the warning level until the end of the enclosing scope. Since the "may fail" message has warning level 6, setting the warning level to 5 will hide it in a particular scope. So, for instance, we could have changed our handler example as follows to get rid of this particular warning but still check for it in other code:

   if P /= null then
       declare
           pragma Warning_Level (5);
       begin
           T := P.all;
       exception
           when Constraint_Error => null;
       end;
   end if;

This is especially useful if the Warnings are Errors mode is used, as any detected warning will prevent the unit from compiling.

The Janus/Ada optimizer is not very smart about what gets modified by calls and indirect writes. This can cause additional false positives. For instance, a call generally is considered to kill all values (that is, potentially change them), so the checking optmizations usually cannot cross a call. That means something like:

   if P /= null then
       Ada.Text_IO.Put("Got here");
       T := P.all; -- (6)

The call will cause a false positive at (6). Rearranging the code or introducing a null-excluding temporary can eliminate these.

A similar effect can happen for an indirect write:

   if P /= null then
       P.all := 10; -- (7)
       T := P.all; -- (8)

A false positive will occur at (8), even though it is obvious (to us) that P is not modified by the assignment to P.all.

We were already planning to make the optimizer smarter in some of these cases, and this effect with the checking warnings will increase the priority of doing that.

With the current version of Janus/Ada, it is often the most useful to use /w5 with /yqc. That turns on the checking quality warnings, but suppresses any warnings about checks that might (but might not) fail. We're using this configuration to compile Janus/Ada; with code not originally designed for these checks, there are thousands of checks where access values are assumed to not be null that aren't documented or asserted. Wading through all of those is unlikely to improve the code much (it's been tested throughly for decades). But getting the warnings for always failing checks and unreachable checks will improve the compiler, as well removing unneeded checks in the case where the compiler can tell that a check cannot fail.

We're still considering how the "check may fail" case should be reported, and whether the check quality warnings should be turned on with the optimizer (rather than separately as is currently the case). Given the value of the always fail and unreachable warnings, perhaps they should be on most of the time. Perhaps the only warning that really needs to be controlled is the "check may fail" warning. More thought and practice is needed on this point.

In future versions of Janus/Ada, we intend to extend this mechanism to most integer-related checks, as well as implementing a similar mechanism for assertions (including Pre and Post).

We'd like to hear from any Janus/Ada user on how the Quality Warnings are working for them. Write me at randy@rrsoftware.com.

Copyright © 2019 RR Software, Inc.
Use of this site constitutes your acceptance of these terms of use.