AdaCore Blog

A Further Expedition into Libadalang: Save Time with Libadalang.Helpers.App

by Pierre-Marie de Rodat

Martyn’s recent blog post showed small programs based on Libadalang to find uses of access types in Ada sources. Albeit short, these programs need to take care of all the tedious logistics around processing Ada sources: find the files to work on, create a Libadalang analysis context, use it to read the source files, etc. Besides, they are not very convenient to run:

$ gprls -s -P test.gpr | ./ptrfinder1 | ./ptrfinder2

The gprls command (shipped with GNAT Pro) is used here in order to get the list of sources that belong to the test.gpr project file. Wouldn’t it be nice if our programs could use the GNATCOLL.Projects API in order to read this project file themselves and get the list of sources to process from there? It’s definitely doable, but also definitely cumbersome: first we need to get the appropriate info from the command line (project file name, potentially target and runtime information, or a *.cgpr configuration file), then call all the various APIs to load the project, and many more operations.

Such operations are so common for tools using Libadalang that we have decided to include helpers to factor this in the library itself, so that programs can focus on their real purpose. The 20.1 Libadalang release provides building blocks to save you this trouble: check the App generic package in Libadalang.Helpers. Note that you can see a tutorial and its API reference for it in our nightly documentation.

This package is intended to be used as a framework: you instantiate it with your settings at the top-level of your program and call its Run procedure. App then takes over the control of the program: it parses command-line options and invokes when appropriate the callbacks you provided it. Let’s update Martyn’s programs to use App. The job of the first program (ptrfinder1) is to go through source files and report access type declarations and object declarations that have access types.

First, we declare some shortcuts for code brevity:

package Helpers renames Libadalang.Helpers;
   package LAL renames Libadalang.Analysis;
   package Slocs renames Langkit_Support.Slocs;

Next, we can instantiate App:

procedure Process_Unit
     (Job_Ctx : Helpers.App_Job_Context; Unit : LAL.Analysis_Unit);
   --  Look for the use of access types in Unit

   package App is new Helpers.App
     (Name         => "ptrfinder1",
      Description  => "Look for the use of access types in the input sources",
      Process_Unit => Process_Unit);

Naturally, the Process_Unit procedure will be called once for each file to process. The Name and Description formals allow the automatic generation of a “help” message on the command-line (see later). Implementing the Process_Unit procedure is as easy as running minor adjustments on Martyn’s original code:

procedure Report (Node : LAL.Ada_Node'Class);
   --  Report the use of an access type at Filename/Line_Number on the standard
   --  output.

   ------------
   -- Report --
   ------------

   procedure Report (Node : LAL.Ada_Node'Class) is
      Filename : constant String := Node.Unit.Get_Filename;
      Line     : constant Slocs.Line_Number :=
         Node.Sloc_Range.Start_Line;
   begin
      Put_Line (Filename & ":"
                & Ada.Strings.Fixed.Trim (Line'Image, Ada.Strings.Left));
   end Report;

   ------------------
   -- Process_Unit --
   ------------------

   procedure Process_Unit
     (Job_Ctx : Helpers.App_Job_Context; Unit : LAL.Analysis_Unit)
   is
      pragma Unreferenced (Job_Ctx);

      function Process_Node (Node : Ada_Node'Class) return Visit_Status;
      --  Callback for LAL.Traverse

      ------------------
      -- Process_Node --
      ------------------

      function Process_Node (Node : Ada_Node'Class) return Visit_Status is
      begin
         case Node.Kind is
            when Ada_Base_Type_Decl =>
               if Node.As_Base_Type_Decl.P_Is_Access_Type then
                  Report (Node);
               end if;

            when Ada_Object_Decl =>
               if Node.As_Object_Decl.F_Type_Expr
                  .P_Designated_Type_Decl.P_Is_Access_Type
               then
                  Report (Node);
               end if;

            when others =>

               --  Nothing interesting was found in this Node so continue
               --  processing it for other violations.

               return Into;
         end case;

         --  A violation was detected, skip over any further processing of this
         --  node.

         return Over;
      end Process_Node;

   begin
      if not Unit.Has_Diagnostics then
         Unit.Root.Traverse (Process_Node'Access);
      end if;
   end Process_Unit;

We’re nearly done! All that’s left to do is to make our program only call the Run procedure:

begin
   App.Run;
end ptrfinder1;

That’s it. Build and run this program:

$ ./ptrfinder1
No source file to process

$ ./ptrfinder1 basic_pointers.adb
/tmp/access-type-detector/test/basic_pointers.adb:3
/tmp/access-type-detector/test/basic_pointers.adb:5

So far, so good.

$ ./ptrfinder1 --help
usage: ptrfinder1 [--help|-h] [--charset|-C CHARSET] [--project|-P PROJECT]
                 [--scenario-variable|-X
                 SCENARIO-VARIABLE[SCENARIO-VARIABLE...]] [--target TARGET]
                 [--RTS RTS] [--config CONFIG] [--auto-dir|-A
                 AUTO-DIR[AUTO-DIR...]] [--no-traceback] [--symbolic-traceback]
                 files [files ...]

Look for the use of access types in the input sources

positional arguments:
   files                 Files to analyze
   
optional arguments:
   --help, -h            Show this help message
[…]

Wow, that’s a lot! As you can see, App takes care of parsing command-line arguments and provides a lot of built-in options. Most of them are for the various ways to communicate to the application the set of source files to process:

  • "ptrfinder1 source1.adb source2.adb …" will process all source files on the command-line, assuming that all source files belong to the current directory;
  • "ptrfinder1 -P my_project.gpr [-XKEY=VALUE] [--target=…] [--RTS=…] [--config=…]" will process all source files that belong to the my_project.gpr project file. If additional  source files appear on the command-line, ptrfinder1 will process only them, but my_project.gpr will still be used to find the other source files.

  • "ptrfinder1 --auto-dir=src1 --auto-dir=src2" will process all Ada source files that can be found in the src1 and src2 directories. Likewise, additional source files on the command-line will restrict processing to them.

These three use cases should cover most needs, the most reliable one being the project file way: calling gprbuild on the project file (with the same arguments) is a cheap way to check using the compiler that the set of sources passed to the application/Libadalang is complete, consistent and valid Ada.

As it is a common gotcha, let’s take a moment to note that even though your application may process only one source file, Libadalang may need to get access to other source files. For instance, computing the type of a variable in source1.adb may require to read pkg.ads, which defines the type of this variable. This is why passing a project file or --auto-dir options is useful even when you pass the list of source files to process explicitly on the command-line.

Martyn’s second program (ptrfinder2) doesn’t use Libadalang, so rewriting it to use App isn’t very interesting. Instead, let’s extend the previous program to run the text verification on the fly. We are going to add a command-line option to our application to optionally do the verification. Right after the App instantiation, add:

package Do_Verify is new GNATCOLL.Opt_Parse.Parse_Flag
     (App.Args.Parser,
      Long => "--verify",
      Help => "Verify detected ""access"" occurences");

App’s command-line parser (App.Args.Parser) uses the GNATCOLL.Opt_Parse library, so adding support for new command-line options is very easy. Here, we add a flag, i.e. a switch with no argument: it’s either present or absent. Just doing this already extends the automatic help message:

$ ./ptrfinder1 --help
usage: ptrfinder1 […]
                 files [files ...] [--verify]

Look for the use of access types in the input sources

positional arguments:
   files                 Files to analyze
   
optional arguments:
[…]
   --verify,             Verify detected "access" occurences

Now we can modify the Report procedure to handle this option:

function Verify
     (Filename : String; Line : Slocs.Line_Number) return Boolean;
   --  Return whether Filename can be read and that its Line'th line contains
   --  the " access " substring.

   procedure Report (Node : LAL.Ada_Node'Class);
   --  Report the use of an access type at Filename/Line_Number on the standard
   --  output. If --verify is enabled, check that the first source line
   --  corresponding to Node contains the " access " substring.

   ------------
   -- Verify --
   ------------

   function Verify
     (Filename : String; Line : Slocs.Line_Number) return Boolean
   is
      --  Here, we could directly look for an "access" token in the list of
      --  tokens corresponding to Line in this unit. However, in the spirit of
      --  the original program, re-read the file with Ada.Text_IO.

      Found : Boolean := False;
      --  Whether we have found the substring on the expected line

      File : File_Type;
      --  File to read (Filename)
   begin
      Open (File, In_File, Filename);
      for I in 1 .. Line loop
         declare
            use type Slocs.Line_Number;

            Line_Content : constant String := Get_Line (File);
         begin
            if I = Line
               and then Ada.Strings.Fixed.Index (Line_Content, " access ") > 0
            then
               Found := True;
            end if;
         end;
      end loop;
      Close (File);
      return Found;
   exception
      when Use_Error | Name_Error | Device_Error =>
         Close (File);
         return Found;
   end Verify;

   ------------
   -- Report --
   ------------

   procedure Report (Node : LAL.Ada_Node'Class) is
      Filename   : constant String := Node.Unit.Get_Filename;
      Line       : constant Slocs.Line_Number :=
         Node.Sloc_Range.Start_Line;
      Line_Image : constant String :=
         Ada.Strings.Fixed.Trim (Line'Image, Ada.Strings.Left);
   begin
      if Do_Verify.Get then
         if Verify (Filename, Line) then
            Put_Line ("Access Type Verified on line #"
                      & Line_Image & " of " & Filename);
         else
            Put_Line ("Suspected Access Type *NOT* Verified on line #"
                      & Line_Image & " of " & Filename);
         end if;

      else
         Put_Line (Filename & ":" & Line_Image);
      end if;
   end Report;

And voilà! Let’s check how it works:

$ ./ptrfinder1 basic_pointers.adb --verify
Access Type Verified on line #3 of /tmp/access-type-detector/test/basic_pointers.adb
Access Type Verified on line #5 of /tmp/access-type-detector/test/basic_pointers.adb

When writing Libadalang-based tools, don’t waste time with trivialities such as command-line parsing: use Libadalang.Helpers.App and go directly to the interesting parts!

You can find the compilable project for this post on my GitHub fork. Just make sure you get Libadalang 20.1 or the next Continuous Release (coming in February 2020). As usual, please send us suggestions and bug reports on GNATtracker (if you are an AdaCore customer) or on Libadalang’s GitHub project.

Posted in #Libadalang   

About Pierre-Marie de Rodat

Pierre-Marie de Rodat

Pierre-Marie joined AdaCore in 2013, after he got an engineering degree at EPITA (IT engineering school in Paris). He mainly works on GNATcoverage, GCC, GDB and Libadalang.