Ada – The Veteran Programming Language

Ada, a descendent programming language mainly of Pascal, is a statically and strong typed, structured and imperative programming language that inherently supports pure object oriented programming, explicit concurrency and tasks, low level access to I/O and data representation, external interrupt handling, synchronous message passing, design by contract and non-deterministic programming techniques. Static Type checking checks type safety before runtime and makes a language compiler too conservative that it ensures that the program will run without errors at run time. This also improves program efficiency on run time. Imperative programming language describes how a program should operate. For example it defines methods or procedures or assignments that tell the program how it needs to operate rather than telling it what it should accomplish. Object orientedness explicitly supports modularity, cohesiveness, data encapsulation and object safety. Explicit concurrency and asynchronous message passing implements explicit ways to ensure that tasks are locked and controlled without room for deadlock or other exceptions. Unlike if-then-else statements, a non-deterministic method applied in a programming language allows the program to choose between certain execution paths based on certain conditions or learning that happens along the execution or at run time based on some background situations. These explicit features that built into Ada makes it one of the most widely used programming language in proven Avionics, transportation, defense, healthcare, Robotics and space systems, where a minor margin of error can cause harm to human life.

Ada was primarily developed around 1977-1983 by J. Ichbiah and colleagues of CII Honeywell-Bull group group in France as per contract from DoD (Department of Defense, US) to be used in embedded software systems. Ada was coined according to Lady Ada Lovelace who is considered as the first programmer and an associate of Charles Babbage. As we saw in the first paragraph of this article, the inherent features of Ada makes it most suited programming language for embedded systems which are classified as mission critical systems where there is zero room for error.

Below are some of the important features of Ada.

1) Type Checking

Type checking refers to the restrictions and limitations imposed on the ways in which data items can be manipulated by the program. Ada employs static strong type checking in which the type declarations are checked statically and dynamically. Subtypes are subranges of integers, record variants and array bounds. THese subtype properties must be checked in runtime. Thus Ada employs strong type checking in which careful distinction is made between static properties and dynamic properties of data types. Since all types are statically checked, it is impossible for a run time error to occur and programmer can be confident that all inputs will pass type checking.

2) Function overloading and operator overloading

Ada employs type checking mechanisms and explicit type conversions to supports function overloading and operator overloading.

Function / sub-program overloading is necessary since strong type checking of the data type of the function parameters needs that there should not be problems when the function handles data of different types. For eg:, the subprogram named Put(c) is overloaded and it could handle integer, real or string parameters and arguments without causing exceptions/errors while strong type checking. Thus Put(c) can handle the following operations –

X : float; X:= 3.0;
Y : integer; Y:= 3;
Z : char; Z:= B;
Put(X); PUT(Y); PUT(Z);

Even though X, Y, Z are of different datatypes, operator overloading permits association of different meanings with a symbol. The meaning of operators is determined by the number and type of operands. Thus, the operator ‘+’ can be overloaded for handling the following functions –

+3 -> as unary +

3 + 4 -> for integer addition

3.0 + 4 -> for float addition

3) Separate Compilation

Ada supports separate compilation of program modules. To secure type checking across the boundaries of separate compilation units, Ada translators produce tables of external reference for each compilation unit which enables the loader to check for the correctness of interfaces between compiled modules, thus allowing independent compilation or separate compilation at the same time preserving the integrity of the separately compiled modules. Also, any changes to any of the modules will instruct the loader to incrementally recompile all the affected modules.

4) User define data types

Ada supports major user defined data types such as sub types, enumerated types, record types and pointer types.

Subtypes refers to the subranges of values defined for an object that the object may take and does not change the set of permitted operations. For eg:, we may define a subtype of char which ranges between A to Z as –

Subtype Letter is char (A..Z);

Enumerated datatypes enables user to define a set of values for the object of a type as,

type day is (S, M, T, W, TH, F, SAT);

The above enumerated type shows that the object day can take any of the values in the shown list so that the set of values are implicit position values i.e value of S = 0, M = 1 and so on.

Record types are similar to structure definition in C. A record in Ada is considered as a composite data object. For eg:, we may define a record in Ada as,

type employee is
    record
          empno : integer;
          empname : string;
    end record;      

Pointer types allows users to point one object to another and is especially applicable in implementing linked lists, queues, trees and other data structures. In Ada, pointer types are called ‘access types’. An access type for a linked list of integers can be defined as follows –

type LINK is access INT_NODE;
type INT_NODE is
  record
       Item : integer;
       Next : LINK;
  end record

Now, the statement –

Head : LINK;

creates an object of type LINK. Now the following statements creates the item nodes as,

Head := new INT_NODE(0, null);

The above statement creates an item node on Head with a value 0 and with Head.Next pointing to null.

Head.Next := new INT_NODE(1, null);

The above statement creates another item node with value (1, null) and stores its address in Head.Next.

5) Data abstraction in Ada

Data abstraction in Ada incorporates the concepts of Data encapsulation and abstract data types. Both the mechanisms allow definition of composite data objects in terms of data object attributes and operations that can be performed on them, by suppressing the details of data representation and data manipulation to outside objects.

Data abstraction in Ada enables definition of data objects which provides object oriented techniques for developing well-structured software. The internal operation details and data representation (or data attributes) may be changed will without affecting a single line of code in the program that uses those data objects, provided the interface of the manipulation procedures remains the same. Also by applying data abstraction, we may define user defined data objects and may develop a hierarchy of object abstractions.

The difference between data encapsulation and abstract data types is that while data encapsulation provide only one instance of entity, abstract data type is a template from which multiple instances can be created.

Data encapsulation

Consider the following program segment in Ada which implements a data encapsulation of a stack entity

package Stack is
       procedure PUSH(I : in INTEGER);
       procedure POP(I : out INTEGER);
       function EMPTY return BOOLEAN;
       function FULL return BOOLEAN;
       STKFULL, STKEMPTY : exception;
end
package body Stack is
       SIZE : constant := 100;
       INDEX : INTEGER range 0..SIZE :=0;
       STORE : array(1..SIZE) of INTEGER;
     begin
          procedure PUSH(I: in INTEGER) is
          begin 
               if FULL then raise STKFULL;
               INDEX : = INDEX + 1;
               STORE(INDEX) : = I;
          end PUSH;
          procedure POP(I: out INTEGER) is
          begin 
               if EMPTY then raise STKEMPTY;
               I = STORE(INDEX);
               INDEX = INDEX -1;
          end POP;
          function EMPTY return BOOLEAN is
          begin
                return(INDEX=0);
          end EMPTY;
          function FULL return BOOLEAN is
          begin
                return(INDEX=SIZE);
          end FULL;
    end STACK;

As shown above, the data encapsulation is enabled by Ada in the following way –

  • The data encapsulation is implemented by Package.
  • The Package consists of a package specification part and a package body.
  • The specification part consists of a visible part anda private section (not shown). The visible part consists of interface specification for the manipulation routines.
  • The package body consists of the details of data representation and data manipulation routines as shown.

Abstract data type

The following program segment implements the abstract data type stack in Ada which may be instantiated into several objects which are physical representation of the abstract data type STACK.

package STACK_TYPE is
    type STACK is limited private;
       procedure PUSH(I : in INTEGER;
                       S: in out STACK);
       procedure POP(I : out INTEGER;
                       S: in out STACK);
       function FULL(S: in STACK) return BOOLEAN;
       function EMPTY(S: in STACK) return BOOLEAN;
       STKFULL, STKEMPTY : exception;
       private
            type STACK is
            record
               SIZE: constant:= 100;
               INDEX: INTEGER range 0..SIZE:= 0;
               STORE: array(1..SIZE) of INTEGER;
            end record;
end STACK_TYPE;
package body STACK_TYPE is       
     begin
          procedure PUSH(I: in INTEGER;
                         S: in out STACK) is
          begin 
               if FULL then raise STKFULL;
               S.INDEX := S.INDEX + 1;
               S.STORE(S.INDEX) : = I;
          end PUSH;
          procedure POP(I: out INTEGER;
                           S: in out STACK) is
          begin 
               if EMPTY then raise STKEMPTY;
               I = STORE(S.INDEX);
               S.INDEX = S.INDEX -1;
          end POP;
          function EMPTY(S: in STACK) return BOOLEAN is
          begin
                return(S.INDEX = 0);
          end EMPTY;
          function FULL(S: in STACK) return BOOLEAN is
          begin
                return(S.INDEX=S.SIZE);
          end FULL;
    end STACK;

The following features may be noted from the above implementation of STACK abstract data type.

  • The abstract data type STACK_TYPE provides the templates for creating multiple object of the type STACK by writing the code –
STK1, STK2:STACK;

where STK1 and STK2 now becomes the objects of type STACK.

  • The visible part of STACK_TYPE consists of interfaces of manipulation routines and private section consists of hidden data representation.
  • The package body, again hidden from the user of the STACK object, consists of details of manipulation routines and are separately compiled.
  • By such a method of using abstract data types, we are thus able to create objects of the type of that abstract data type’s template. The user of the object STACK this see only the package specifications and the interface routines and never sees the private data representations or the codings in the package body.
  • The user of object of type STACK may invoke the visible functions and procedures from inside the package as PUSH(2, STACK) or FULL(STK1) etc .. for performing Stack operations.

Generic classes for creating program templates

Another important application of data abstraction in Ada is the generic clause which enables us to create program templates. The speciality of such templates is that they allow program execution for any type of parameters, may it be integer, float, link list, queue etc. On run time, the Ada translator will replace the templates with the actual parameters. For the stack operation, we use the following ‘generic’ clause for implementing the template.

generic
SIZE: NATURAL;
type ELEMENT is private;
             :      

The rest of the code is similar to the code listed above under abstract type, except that the INTEGER type is replaced by ‘ELEMENT’ type ad the name STACK_TYPE is changed to GENERIC_STACK. Now in such an operation, we may define,

Generic packages such as,

package INT_STK is new GENERIC_STACK(100, INTEGER);

package BOOL_STK is new GENERIC_STACK(50, BOOLEAN);

package ARRAY_STK is new GENERIC_STACK(10, array(1..10) of REAL);

We may create objects as,

INT_STK1: INT_STK.STACK;

and manipulate it as,

PUSH(3, INT_STK1), FULL(INt_STK)

We may also create objects of the ARRAY_STK type as,

ARRAY_STK1: ARRAY_STK.STACK;

and can be manipulated as,

PUSH((X(3). ARRAY_STK1), FULL(ARRAY_STK1) etc.

5) Exception handling in Ada

Ada enables users to implement exception handling mechanisms which may be a combination of user defined exceptions and exception handling code or may be a combination of systems defined exception and user defined exception handling code. Ada has an exception handler which handles all user-defined exceptions explicitly and all system defined exceptions implicitly. The user can specify exception handlers for several predefined exception conditions. If the user does not specify an exception handler for a predefined exception condition, a system defined exception handling mechanisms in Ada.

Termination model of exception handling

In termination model of exception handling, the program unit is automatically terminated after the control passes to the exception lock and it is executed. Consisder the following code segment implementing termination model of exception handling in Ada.

Procedure P() is
--declarations
SINGULAR: exception;
--executable statements

--user defined exceptions
if(COND) then raise SINGULAR;
  --executable statements
exception
    when SINGULAR =>
         --exception handling code for SINGULAR condition;
    when NUMERIC_ERROR =>
         -- user-supplied exception handler for the system-defined         exception NUMERIC_ERROR;
     raise;
end P;

In the above code, as the exception SINGULAR is raised, the control of program passes to the exception block and the exception handling code for SINGULAR condition is executed and then procedure is terminated. Now, when the predefined exception NUMERIC_ERROR occurs, the control pases to the exception block and the user supplied exception handle is executed and procedure is terminated.

Resumption model of exception handling

In resumption model of exception handling, we apply the following procedure.

Consider the code segments,

X:= P+ Q;
Y:= X-Q;

Now, in normal situations, Y will have the value of P. But if overflow occurs while adding P & Q, Y need not be equal to P. In such cases, as an exception handling mechanism, we assert by adding Y:=P; when control reaches the point Y:= X-Q

5) Concurrency Mechanisms

Ada enables programmers to implement concurrent threads / processes of program statements executing in parallel. Thus we will be able to perform an I/O operation and a file read operation at the same time. For proper concurrency control, Ada use various synchronization mechanisms to enable information transfer between concurrently executing tasks, avoid simultaneous data updations and avoid deadlocks. The following are commonly used concurrency control mechanisms in Ada.

Use of Semaphore variables

Semaphore variables (S) may be used to synchronize concurrently executing tasks via synchronising functions wait() & signal(). WE may use Boolean Semaphore or integer semaphores according to the application domain.

Boolean Semaphores

Boolean semaphores have two states – ‘true’ or ‘false’. The wait(S) and signal(S) have the following synchronisation operations.

wait(S): If S is ‘true’, the task issuing wait(S) is suspended. If S is false, it is set to true and the task issuing wait(S) proceeds. signal(S): S is set to false. If there are tasks suspended by wait(S) operations, one of them is allowed to proceed.

An example for boolean semaphore operation for synchronising two tasks accessing shared data is as shown below –

S = false;
Task A;
wait(S);
--Access shared data
signal(S);
Task B;
wait(S);
--Access shared data
signal(S);

Initially, S=false and A starts with the first CPU time slice. Now, as wait(S) is executed, since S = false, it is set to true (task B is suspended) and A accesses the shared data and executes signal(S) operation. This time, S is set to false and the waiting task B proceeds. This time, again, S=true on execution of wait(S) by B(i.e A is suspended) and the process goes on so that tasks A & B are synchronized.

Integer Semaphores

Integer semaphores can have more than two values. This is useful to permit access to a resource by a limited number of copies of a resource. In this case, we define wait(S) & signal(S) operations as –

wait(S): If S = 0, the issuing wait(S) is suspended
       else if S>0 then S becomes S-1 and the task issuing wait(S) is allowed to continue
       else error
signal(S): If S=0 then S = S + 1 and one waiting task is allowed to proceed and the waiting task will execute its wait(S) operation to decrement S.
           elseif S > 0 then S = S + 1 and there are no waiting tasks
           else error

Asynchronous message passing by producer – consumer mechanism

Consider the following code segment in Ada which implements asynchronous message passing to apply concurrency control –

SEMAPHORE: E,F,M;
INITIALLY: E:=N;F:=0;M:=1;

--Task1
loop
  --produce item;
wait(E);signal(M);
--place an item in buffer;
signal(F);signal(M);
end loop

--Task2
loop
    wait(F);signal(M);
  --remove item from buffer;
    wait(E);signal(M);
signal(E);signal(M);
--consume item;
end loop

The above concurrency control mechanism has the following features –

  • E is the number of empty cells semaphore, F is the number of full cells semaphore, M is the mutual exclusion semaphore
  • The above synchronization mechanism makes the producer task1 to place an item in buffer only when the consumer has removed one from the buffer in case of full buffer condition and the consumer task2 to remove an item from the buffer only when the producer has placed one in the buffer in case of an empty buffer condition.