Package TRACER (V2.3)


Files in this distribution: package Tracer specification
tracer.adb package Tracer body package Tracer_Messages. It is actually a renaming of Tracer_Messages_English or Tracer_Messages_French, allowing Tracer to speak your preferred language. Contributions for other languages welcome! Package Tracer_Messages_English, specification (no body required). Contains all messages used by Tracer, English version. Package Tracer_Messages_French, specification (no body required). Contains all messages used by Tracer, French version package Tracer.Assert specification
tracer-assert.adb package Tracer.Assert body package Tracer.Timing specification.
tracer-timing.adb package Tracer.Timing body.
tracer.html (this file) package Tracer documentation
tracer_implementation.html package Tracer implementation notes
ttracer.adb test program and example for package Tracer auxiliary package for test program (spec)
ttracer2.adb auxiliary package for test program (body) package Protection specification
protection.adb package Protection body
protection.html package Protection documentation
COPYING GNU General Public License

Package Tracer makes use of package Protection, which is available separately from Adalog's software components page. As a convenience, we provide it with this distribution of Tracer, although it is an independent component.


Fully portable to any compiler supporting System-Programming and Real-Time annexes.
This package uses no compiler specific feature; it uses features defined in the System-Programming and Real-Times annexes, and does not use any feature defined in other annexes.

Copyright, etc.

The Tracer component (Packages Tracer, Tracer.Assert and Tracer.Timing and the associated documentation) is Copyright 1997, 2021 ADALOG.

The Tracer component is free software; you can redistribute it and/or modify it under terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This component is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License distributed with this program; see file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

As a special exception, if other files instantiate generics from the units of this component, or if you link units provided with this component with other files to produce an executable, these units do not by themselves cause the resulting executable to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the executable file might be covered by the GNU Public License.

This component is offered with this very liberal license as a demonstration of Adalog's know-how; please do not remove the reference to Adalog in the header comment of each unit.

Although we do not require it, you are very welcome, if you find this component useful, to send us a note at We also welcome critics, suggestions for improvement, etc.

About Adalog

ADALOG is providing training, consultancy, expertise and custom development in Ada and related software engineering techniques. For more information about our services:

2 rue du Docteur Lombard
Tel: +33 1 41 24 31 40
Fax: +33 1 41 24 07 36

What package Tracer does

This package provides a sophisticated trace facility, especially valuable for multi-tasking programs. The child package Tracer.Timing also provides utilities for measuring execution time, and the child package Tracer.Assert provides utilities for checking that certain properties of your program are actually met. To get an idea, compile and run the test program (ttracer.adb).

The user interface of Tracer can be taylored to speak various languages; currently, English and French are supported, and English is the default. If you want Tracer to use another language, please read the comments in package Tracer_Messages. We will gladly accept any voluntarily effort to support other languages.

To use the package, you put calls to various Trace procedures into your code (don't forget to "with" package Tracer and/or package Tracer.Timing or package Tracer.Assert). You can specify when you enter or exit a procedure, so that trace messages are properly indented. Indentation is kept on a per-task basis, and traces from different tasks are clearly separated. You can even chose to trace messages only from a given, or several, tasks (or except from one or several tasks).

When you start your program, Tracer is automatically activated and pauses with the following message:

**Tracer** : Pause (Trace Mode: Enter=Normal, 'S'=Step, 'I'=Ignore Tracer)
**Tracer** Command? ('H' for Help) :

These are the most common answers at that point, but actually, any response allowed at a regular pause (see Controlling traces) is allowed.

Once your program is debugged, remove the trace calls and the "with Tracer;" from your code. Remove the Tracer package itself from your path, and recompile. If you left inadvertantly some trace calls, the compiler will tell you... This is why it is MUCH better to use this package rather than simple calls to Put_Line.

The basic trace procedures

procedure Trace (Message : String);

This is the simplest trace procedure. It simply prints its argument, according to the current indentation level. If the message includes control characters, they are translated to ^X notation (i.e. a CR will be displayed as ^M). This can be handy in dectecting problems caused by control characters in strings.

type Trace_Flag is (Start, Stop);
procedure Trace (Flag : Trace_Flag; Message : String);

This procedure is intended to trace subprograms calls. Put a Trace(Start,...) when you enter a subprogram, and a Trace(Stop,...) when you exit it. The message can be anything, but generally you will like to put the name of the entered subprogram here. Entering and exiting subprograms is marked with arrows ("=>" or "<=") in the trace, and the indentation level is changed accordingly.

type Tracer_String (<>) is limited private;
type Tracer_String_Access is access Tracer_String;
function "+" (Item : String) return Tracer_String_Access;

type Auto_Tracer (Message : Tracer_String_Access) is limited private;

Alternatively, you can declare an object of type Auto_Tracer, initialized with the name of the subprogram, such as in:

Tracer: Auto_Tracer (+"My_Subprogram");

The initialization of this object will automatically call Trace (Start, ...), and its finalization will call Trace (Stop, ...). It is especially convenient for tracing functions that return complex expressions, since the Finalization (and therefore the call to Trace (Stop...)) happens after the evaluation of the return expression.

procedure Trace (Message : String; Value : Boolean);

This procedure allows you to trace boolean values. It prints the message, followed by the image of Value.

type Any_Int is range System.Min_Int .. System.Max_Int;
procedure Trace (Message : String; Value : Any_Int);

This procedure allows you to trace integer values. It prints the message, followed by the image of Value. Since the type Any_Int is compatible with any integer type, you can call it as:

    Trace("Value of X:", Any_Int(X));

whatever the type of X is.

type Any_Float is digits System.Max_Digits;
procedure Trace (Message : String; Value : Any_Float);

This procedure allows you to trace float values. It prints the message, followed by the image of Value. Since type Any_Float is compatible with any floating point type, you can call it as:

    Trace("Value of X:", Any_Float(X)); 

whatever the type of X is.

procedure Trace (Message : String; Raised_Exception : Ada.Exceptions.Exception_Occurrence); 

This procedure is useful to trace exceptions. It prints the message, followed by the exception name of Raised_Exception. It is typically put in an exception handler :

   when Occ : others => 
      Trace ("Unexpected exception raised : ", Occ);
procedure Trace (Message : String; Value : Ada.Tags.Tag); 

This procedure allows you to trace tags (it prints the expanded name of the tag). Typically, when you expect a given tagged type and the one you get is not the one you expected, you can write:

    Trace("Tag of X:", X'Tag); 
type Tracer_Stream_Access is access all Ada.Streams.Root_Stream_Type'Class;
Trace_Stream : constant Tracer_Stream_Access;

This stream allows you to trace any type, by writing something like:

   X : T;
   T'Write (Trace_Stream, X);

Output will be the hexadecimal representation of the stream elements and their interpretation as Ascii characters. Note that the representation can be compiler dependent, so check you compiler's documentation to interpret the values; especially note that on little endians (Intel) platforms, bytes may be swapped. Note also that for composite values, this will result to a call to 'Write for each elementary type (unless you have redefined the 'Write attribute), so you will obtain several trace messages. Any use of 'Read on this stream will raise Program_Error.

type Tracer_Counter is range 1..25;
function Tracer_Count (Counter : Tracer_Counter) return String;

Tracer provides 25 counters. Each call to Tracer_Count increments the corresponding counter and returns its value as a string. This can be useful to trace where you are in a loop:

   Trace ("Iteration" & Tracer_Count (1));
procedure Pause;
procedure Pause (Message : String);

These procedures simply pause (i.e. stop execution and wait for input); this allows you to view the trace messages. At this point, you are also given the opportunity to change trace parameters; see Controlling traces for more details. The form without a parameter prints a default message, and the form with a Message prints the message when the Pause is executed. Note that it is not equivalent to calling Trace followed by Pause without Message, because in the latter case some other task could insert its own messages inbetween.

The message is output to the current target, but the pause prompt is always printed on the console. In other words, if you are tracing to a file, the pause message will not appear on the terminal. However, if you issue a '?' command, it will show you the current pause message.

Delayed output

procedure Keep_Trace (...);
procedure Flush_Trace;

Sometimes, you don't want Tracer output to be mixed with your own output. In this case, use Keep_Trace instead of Trace. There is a Keep_Trace procedure for each Trace, with the same parameters (actually, there is an extra parameter, but you can ignore it for now, the default is OK).

With Keep_Trace, the message is saved, and will be printed as soon as you call any regular Trace, or you can force output by calling Flush_Trace. Of course, proper ordering of trace messages is preserved. If your program terminates, whether normally or abnormally, for any reason, all outstanding kept messages are printed.

Controlling traces

Trace messages originate from source tasks and are directed to a target file. By default, all tasks in the program are sources, and the target file is the console, but this can be changed either by calling subprograms provided by package Tracer, or by giving commands when the program pauses. We give here the description of the commands; corresponding subprograms are described under Advanced features.

Whenever the program pauses, you are prompted with the following message :

Command? ('H' for help) :

At that point, you may simply type 'Enter', or a string consisting of characters representing trace commands. Execution will resume after all commands have been analyzed, if the string includes any of the "Go" commands described below. "Enter" alone is equivalent to repeating the previous "Go" command. The key letters described here are for the English version of Tracer; if you compiled Tracer for a different language, please refer to the Tracer_Messages_XXX package for your language.

Tracing multi-tasks programs

You can use the previous facilities normally from several tasks. The package is completely protected for multi-tasking, so outputs (and pauses) from several tasks will be properly serialized, and no race condition can occur within Tracer nor can any deadlock occur due to Tracer.

When Tracer discovers a call that does not come from the main task, it enters multi-tasking mode. In this mode, trace messages are prefixed with the Task_ID (except for messages that originate directly from Tracer internal tasks). When it enters this mode, Tracer tells you the Task_ID of the main task, so you can identify it in the subsequent traces.

Whenever there is a task switch, traces are separated with a line of hyphens. Indentation levels are kept on a per-task basis, so they are meaningful even in multi-task mode. Note also that task IDs are at the beginning of the line: if you output the trace to a file, and then sort the file (taking as a key the text before the colon), you'll get a complete trace for each task independently. Under Unix, the magic formula is:

   sort -s -t: +0 -1 trace >trace.sorted

Another use is to "grep" the trace file with the task_ID to extract all messages output by a given task.

Tracing protected calls

There are special issues if you want to trace calls to protected operations.

  1. you are not allowed to call potentially blocking operations from a protected operation. Keep_Trace procedures are not potentially blocking, so it is safe to call them. All other procedures are potentially blocking.
  2. Current_Task is not meaningful if called from within a entry body. Since Keep_Trace cannot call Current_Task to know the currently executing task, you must tell it; that's why all Keep_Trace procedures have an extra parameter (Caller) of type Task_ID. The default value is Current_Task, but from an entry body, you should provide it with the 'Caller for the entry. In short, use it as:
         entry body E when ... is 
           Keep_Trace (Start, "Entering E", Caller => E'Caller);
           Keep_Trace (Stop, "Leaving E", Caller => E'Caller);

    Note that if you forget to mention the Caller parameter in a Keep_Trace in an entry body, GNAT will warn you with the message
    "Current_Task" should not be used in entry body (RM C.7(17))

Marking tasks

A task may be marked, and you can chose to trace only marked tasks. In other words, when a Trace (or Keep_Trace) procedure is called, the request is ignored if the calling task is not marked. Conversely, you can chose to trace all but the marked tasks; see Controlling traces.

The mark status of a task can be toggled during a pause (see Controlling traces). Alternatively, it can be set (and retrieved) by program:

package ATI renames Ada.Task_Identification;
procedure Mark      (Target_Task : ATI.Task_ID := ATI.Current_Task)
procedure Unmark    (Target_Task : ATI.Task_ID := ATI.Current_Task)
function  Is_Marked (Target_Task : ATI.Task_ID := ATI.Current_Task) return Boolean;

Note that by default these procedures mark or unmark the current task, but that you can mark or unmark any task. This allows for example the main program to mark the tasks that are to be traced, or a server to mark its clients (thanks to the E'Caller attribute).

Initialization file

During its initialization, Tracer checks for a file named trace.ini in the current directory. If it exists, it reads its command from this file, and returns to the console when the file is exhausted. Blank lines and lines whose first character is '#' are ignored. While reading from the file, all confirmation questions (whose responses are Yes/No) allways default to Yes and are not read from the file nor from the console. This means that for example, if you are tracing to a file and the file already exists, Tracer will automatically overwrite it without asking the question. This is intended to allow the same script to work whether the trace file exists or not.

As a special case, if the file trace.ini exists and is empty (like a symbolic link to /dev/null under Unix for example), Tracer will automatically enter "Ignore" mode and will not print anything. This allows leaving calls to Tracer in a completely silent way, in order, for example, to provide "beta" versions to your testers. If something goes wrong, you just need to delete the trace.ini file to reactivate Tracer.

Advanced features


type Procedure_Access is access procedure;
procedure Set_Watch (To : Procedure_Access);

This procedure allows you to pass a pointer-to-procedure to Tracer. The procedure will be called during a pause when the 'W' command is issued (see Controlling traces). Such a procedure can be used to print the values of internal variables, thus providing a kind of "watch" facility. All regular Tracer procedures can be used from the watch procedure.

Source and Target control

type Trace_Source is (All_Tasks, Marked_Only, Marked_Excepted);
procedure Set_Source (To : Trace_Source);
function  Current_Source return Trace_Source;

These subprograms allow you to set or query the source tasks at any time.

type Trace_Target is (Console, File, None);
procedure Set_Target (To : Trace_Target);
function  Current_Target return Trace_Target;
File_Trace_Denied : exception;

These subprograms allow you to change or query the current target. There is no restriction: you can switch freely between File and Console for example. The first time you switch to File, if a file named trace already exists, the user is asked for permission to overwrite. If overwriting is denied, then the File_Trace_Denied exception is raised.

Control of program termination

procedure Abort_Main; 

This procedure unconditionnally terminates your program, including all tasks. All kept messages are printed. This is useful when you have reached the point where your program is having trouble, and you want to stop it immediately without caring for task terminations. See caveat however.

   with procedure To_Do;
   Label : String := "";
package Last_Will is

end Last_Will;

When instantiated, this (empty) package registers the procedure passed as To_Do to execute it after the main program is left. This is quite similar to a "watch" procedure, except that the given procedure is automatically called after program termination (for whatever reason, of course, including general abort). Any Tracer service can be called from the To_Do procedure. This can be quite handy to check, for example, that a counter of ressources has reached 0 when the program terminates. It is also possible to give Tracer's procedure Pause as a To_Do procedure, therefore giving you a last chance to check your program after the main program has been left.

This package can only be instantiated at accessibility level 0 (i.e. in a library package, but not in a procedure, not even the main program). We strongly recommend to instantiate this package at the very end of the containing package; this will ensure that your To_Do procedure is called before anything in the package gets finalized.

Triggering actions at a given time

type Procedure_Access is access procedure;
Off : constant Duration := 0.0;
procedure Set_Timer (How_Long   : Duration; Processing : Procedure_Access := null); 

This subprogram allows you to change the remaining time before the timer is triggered, and optionaly the procedure called when it is triggered. You can restart the timer at any time, or disable it by calling Set_Timer(Off).

If you pass a pointer to a parameterless procedure as the second argument (Processing), the provided procedure will be called instead of Pause (the default). This is useful if you want to print the value of variables, etc. once you're deadlocked. Note however that given accessibility rules, the provided procedure must be global. And YES, Trace (and even Keep_Trace) procedures can safely be used from the Processing procedure. You may also wish to call Set_Timer from your provided procedure in order to restart the timer; this allows you to periodically sample the state of your program. Another common usage is to pass Abort_Main as the Processing procedure, in order to kill your program once it is dead-locked. If you don't specify Processing, the current Processing procedure is left unchanged.

function Remaining_Time return Duration;

This function returns the time remaining until the timer is triggered.

procedure Suspend_Timer;
procedure Resume_Timer;

Suspend_Timer allows you to temporarily suspend the timer; remaining time is saved. The timer will restart with the saved value when you call Resume_Timer.

Note: The timer is disabled as soon as a general abort is in progress, as a consequence of a 'q' command or of a call to Abort_Main. Calls to timer-related subprograms are ignored.


Using Abort_Main, whether directly or by leaving the program by answering 'q' to a pause, is very demanding on the tasking implementation. It relies on a very precise semantic between abort and finalization which is difficult for compiler-makers to implement.

As of currently, we have tried package Tracer on Gnat 3.15p for Linux, Gnat 3.15p for Windows, and ObjectAda 7.2.1a for Windows, as well as on earlier versions of these compilers. Some had problems when Abort_Main was used. Observed behaviours were :

  1. Everything is OK and works as it should.
  2. Program terminates with a message about an unhandled exception (generally Tasking_Error, but it may be another one). However, everything is OK, only the message is wrong.
  3. Program hangs after printing all messages.
  4. Constraint_Error is raised in the run-time after printing all messages.
  5. Tasking_Error is raised in the run-time after printing all messages.

This is not too worrysome since all messages are printed in any case, but the exit may not be very clean, or, in the case of 3), you may have to kill the process manually. If you have the opportunity to try Tracer with other compilers, please keep us informed how it behaved by sending a note to

What package Tracer.Timing does

This package provides a simple timing facility for measuring execution time of code. We wanted the timing facility itself to be as short as possible, in order to minimize the execution time of the procedures and provide more accurate timings. As a consequence, this package is not protected against tasking, reentrancy, or even recursion.

The package provides 10 timers (this can be changed by modifying the constant Max_Timer in the specification - no other change is required). Each timer can be started and stopped at will, it keeps a record of the total time and the number of times it was started. A Report procedure displays these values, as well as the average execution time.

Timer control procedures

Max_Timer : constant := 10;
type Timer_Index is range 1..Max_Timer;
procedure Start (The_Timer : Timer_Index);

This procedure starts the given timer. If the timer is already started, the call is ignored.

procedure Stop (The_Timer : Timer_Index; With_Trace : Boolean := False);

This procedure stops the given timer. If the timer is not started, the call is ignored. If With_Trace is True, a trace message is printed giving the time elapsed between Start and Stop for this timer.

type Auto_Timer (The_Timer : Timer_Index; With_Trace : Boolean) is limited private;

Alternatively, you can declare an object of type Auto_Timer, such as in:

Tracer: Auto_Timer (The_Timer => 5, With_Trace => False);

The initialization of this object will automatically call Start on the given timer, and its finalization will call the corresponding Stop. If With_Trace is True, a message reporting the time spent in the subprogram is printed for each call.

Using the auto-timer is especially convenient for timing procedures with multiple returns, or functions that return complex expressions, since the Finalization (and therefore the call to Stop) happens after the evaluation of the return expression. On the other hand, controlled types add some overhead, therefore the measurement is less accurate.

procedure Reset (The_Timer : Timer_Index);

This procedure resets the timer to the initial state; all associated counters are cleared.

Reporting and naming

procedure Name (The_Timer : Timer_Index; As : String);

This procedure associates a name (the String argument) with the timer; if defined, the name will be printed by the Report procedure. This is useful if you don't remember what you are timing!

If the timer is active or has never been activated, an appropriate message will tell you.

procedure Report (The_Timer : Timer_Index);

This procedure prints the timer number, its associated name if any, the total time spent between calls to Start and Stop, the number of timing sessions, and the average time (total time divided by number of calls). Note that printing is done by calling Trace; tracing conditions and target are determined by the current state of Tracer.

procedure Report_All;

This procedure calls Report for all counters whose call count is not zero. It is automatically invoked when the program terminates. This way, you don't need to bother about reporting: just put Start and Stop calls around the pieces of code you want to measure, you'll automatically get the statistics when the program terminates.

Note also that the profile of Report_All is conformant with the type Procedure_Access: you can use it as a watch procedure.

What package Tracer.Assert does

This package supports run-time assertion, i.e. checking that certain things happen (or don't happen) in your program.

procedure Check (Assumption : Boolean; Message : String);

If Assumption is False, calls Trace with the given message.

Max_Detector : constant := 10;
type Detector_Index is range 1..Max_Detector;
type R_Detector (Num : Detector_Index; Message : Tracer_String_Access) is limited private;

This is intended to help you discover reentrancy problems, i.e. when two tasks call some subprograms at the same time when they should not. If a task calls a subprogram which declares an object of type R_Detector (shorthand for Reentrancy Detector) while another task is executing a subprogram which also declares an object of type R_Detector with the same Num value, Trace is called with a message identifying the tasks, and both messages. For example, detecting plain reentrancy in a subprogram just requires you to declare:

Detector : R_Detector (1, +"My_Subprogram_Name");
For the definition of type Tracer_String_Access, see "The basic trace procedures". If you need more different detectors, simply change the value of the constant Max_Detector, no other change is necessary.

Questions and answers

Q:How do I skip the next 100 messages?
A:From a pause, give the command "NG100": set target to none, pause after 100 messages. Then resume with "CS" for example: set target to console, step.

Q:What is the difference between the "N" command (trace to None) and the "I" command (Ignore Tracer)?
A: "N" temporarily disables output, but Tracer is still active. All pauses are executed, giving you a chance to resume traces. When you issue an "I" command, Tracer is totally deactivated, there is no way to reestablish it (actually, it even stops its internal tasks and releases as much space as it can).

Q: How to identify which tasks corresponds to a task ID ?
A: Start the task with a trace like : Trace ("This task is the controller"); Since the message shows the task-id, you'll know the correspondance. Note that the task ID image provided by Gnat includes the task's name.

Q: There is a Trace function for floating-point types, but not for fixed-points. Why?
A: It is more difficult to define an "Any_Fixed" type than an "Any_Float". And since fixed points can be converted to floating points, it is just as easy to trace fixed point values with the trace function for floating points.

Q: Why provide a fixed number of counters? It seems simple to declare a Counter type for example...
A: Yes it is, but this would require the user to declare such objects. The idea of Tracer is that you just add calls to Trace, and as far as possible, you don't need to add anything else, especially declarations. 25 counters should be more than enough; normally, you don't leave many traces in your program, you add them when you have a problem, and you remove them when you have found the cause. Therefore, we favour ease of editing over generality. Of course, this is due to the particular context of Tracer, not applicable to general Ada programming!

Q: Why provide two different Pause functions, with and without message, rather than simply a Pause with a default value ?
A: The parameterless Pause can be provided as the Processing action to the timer, but not the other one.

Q: During a pause, I asked to trace only marked tasks, and I had only one such marked task. However, after I typed 'G', I got messages from other tasks.
A: Those messages were sent by other tasks while you were in the pause (don't forget that during a pause, other tasks continue to execute in the background). Of course, the messages were blocked until you typed 'G', and they appear as soon as you restart execution.

Q: After a 'Q' answer during a pause, the program doesn't seem to stop, it even executes pauses...
A: As with the previous question, you still get messages which were sent during the the pause, and also messages that are sent by finalization routines executed as a consequence of the general abort. If you have some automatic pause (as in Step mode), they are normally executed. If you want to leave immediately and silently, type "IQ" (Ignore and Quit).

Q: I call my 'watch' procedure from a Pause, but nothing is printed...
A: If you are currently tracing to a file, traces from your 'watch' procedure go to the file, like all other traces. Set traces back to console ('C'), call your watch procedure ('W'), and then return the trace to the file ('F').

Q: Can I trace finalizations ?
A: Yes, and no message will ever be lost, even if you finalize your own global variables due to an abort of the main task... Guaranteed!

Q: Are you really, REALLY sure that no message can be lost ?
A: OK, if you insist :-)... There is one case where messages are lost: if you run short of memory, since Tracer uses allocators. However, in that case, you'll see a message telling you that messages have been lost. And of course, if you later release space, Tracer will resume its normal operation and the messages will appear at the right place. Note that the task-Id of this "Messages lost" message is the one of the first lost message.

Q: How can I trace deadlocks with protected types ?
A: Trace your protected operations (with Keep_Trace, of course); In the main program, call Set_Timer with a duration long enough to allow the dead-lock to occur. The main program will pause at that time, all kept messages will be printed, and you'll be free to call a user watch procedure to get more information.

Q: I gave a 'D' command with a delay value, but Tracer still says that the timer value is infinite...
A: You are pausing while a general abort is going on ('q' command, or a call to Abort_Main). The timer is disabled in this case. If you issue a '?' command, Tracer will tell you that a general abort is in progress.

Implementation notes

If you are curious about how package Tracer works, you can view the implementation notes. But of course, this is not necessary to use the package.

A final note...

If you found this package useful...
If you think that it would have taken an awful time to write it yourself...
If it showed you some usages of Ada that you didn't think about...

Maybe you should consider using Adalog's consulting and training services !