Freitag, 28. Dezember 2012

Type introspection in Ada

Type introspection is a language feature which allows to determine properties of a type at run-time. Ada is a statically typed language so no introspection is needed, most of the time. When it comes to class-wide types their instances are not so static.

One might think that Ada does not support type introspection, which is almost true, but not quite true. In fact, Ada (starting with Ada 95) has features which can be used to introspect types. This is stream-oriented attributes (RM 13.13.2). The language generates default implementations of T'Read and T'Write based on the type contents. Of course, the compiler knows the type components in order to be able to generate these implementations. Can this knowledge be used for run-time introspection? Yes it can.

We start with the provisions of RM 13.13.2(9):

"... For type extensions, the Write or Read attribute for the parent type is called, followed by the Write or Read attribute of each component of the extension part, in canonical order. ..."
This gives us an idea of calling T'Write on the container type noticing what its implementation would call for the components.

First we declare the abstract base type of the container:

   type Container is abstract tagged limited null record;

   procedure Introspect

             (  Stream : access Root_Stream_Type'Class;

                Item   : Container

             )  is null;

   for Container'Write use Introspect;

Then the abstract base type of the members:

   type Member is abstract tagged limited null record;

   procedure Introspect

             (  Stream : access Root_Stream_Type'Class;

                Item   : Member


   for Member'Write use Introspect;

And finally a fake stream type which will collect the information of the container type contents:

   package List_Of_Tags is
      new Ada.Containers.Vectors (Positive, Tag);

   use List_Of_Tags;

   type Scriber is new Ada.Streams.Root_Stream_Type with record

      List : Vector;

   end record;

   procedure Read

             (  Stream : in out Scriber;

                Item   : out Stream_Element_Array;

                Last   : out Stream_Element_Offset

             )  is null;

   procedure Write

             (   Stream : in out Scriber;

                 Item   : Stream_Element_Array

             )   is null;

The read and write implementations do nothing. The stream contains a vector of type tags, written from the implementation of member's write:

   procedure Introspect

             (  Stream : access Root_Stream_Type'Class;

                Item   : Member

             )  is


      Scriber'Class (Stream.all).List.Append
      (  Member'Class (Item)'Tag

   end Introspect;

This is basically all. Putting the above in a package, e.g. Introspection, it could be used as follows:

with Ada.Tags;       use Ada.Tags;

with Ada.Text_IO;    use Ada.Text_IO;

with Introspection;  use Introspection;

procedure Test is

   type A is new Member with null record;

   type B is new Member with null record;

   type C is new Container with record

      X : A;

      Y : B;

   end record;

   Object   : C;

   Contents : aliased Scriber;


   C'Write (Contents'Access, Object);

   for Index in Contents.List.First_Index
             .. Contents.List.Last_Index

      Put (Integer'Image (Index));

      Put ("  ");

      Put (Expanded_Name (Contents.List.Element (Index)));


   end loop;

end Test;

This will print:

 1  TEST.A

 2  TEST.B

Final notes:
  1. The derived types of Container and Member should not override stream attributes. This will make the compiler to generate them.
  2. The method works recursively, that is you can have container types put into containers all introspected. The trick works as follows. From T'Write of the nested container you can introspect it using another stream object and use the information obtained.
  3. Be careful introspecting from Initialize. When Initialize is called, the derived type's Initialize is not yet completed, i.e. the type is not fully constructed. This is also the reason why T'Write is better to use than T'Read, even if T'Read does not actually update the object.