AdaCore Blog

Let's play '7 differences in Ada'

Let's play '7 differences in Ada'

by Quentin Ochem

The Rules of the Game

Over the past few years, we've been progressively developing new GNAT extensions for the Ada programming language, which you can follow and contribute to on the github RFC page. While the implementation is ramping up, we can already share a few aspects of the language that can be tested with recent versions of the GNAT Pro compiler, which will eventually find their way to the open-source version.

Let's play a game. The code below is an - arguably over-engineered - version of a tic-tac-toe game. Can you find the 7 differences (or features) introduced by the Ada extensions? Note that some of these are only visible on one line, some have an impact across the entire application.

If you want to compile this code, you will need both a GNAT Pro wavefront with the latest changes in, and the switch -gnatX0 that enables all experimental features


package Tic_Tac_Toe is

   type Cell is tagged record
      Symbol : Character;
      Pre : Character;
      Post : Character;
   end record
   with Finalizable => (Initialize => Init),
        Size'Class => 128;

   procedure Print (C : Cell);

   procedure Init (C : in out Cell);

   type Empty_Cell is new Cell with null record;

   type X_Cell is new Cell with null record;

   overriding
   procedure Init (C : in out X_Cell);

   type O_Cell is new Cell with null record;

   overriding
   procedure Init (C : in out O_Cell);

   procedure Process;

end Tic_Tac_Toe;
with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Tags;            use Ada.Tags;

package body Tic_Tac_Toe is

   procedure Print (C : Cell) is
   begin
      Put (C.Pre & C.Symbol & C.Post);
   end Print;

   procedure Init (C : in out Cell) is
   begin
      C.Pre := ' ';
      C.Post := ' ';
      C.Symbol := ' ';
   end Init;

   overriding
   procedure Init (C : in out X_Cell) is
   begin
      C'Super.Init;

      C.Symbol := 'X';
   end Init;

   overriding procedure Init (C : in out O_Cell) is
   begin
      C'Super.Init;

      C.Symbol := 'O';
   end Init;

   type Board_Column is array (Integer range 1 .. <>) of Cell'Class;

   type Board is array (Integer range 1 .. <>) of Board_Column (1 .. 3);

   procedure Process is
      Empty : Empty_Cell;
      X : X_Cell;
      O : O_Cell;

      Game_Board : Board (1 .. 3) := (others => (others => Empty));

      procedure Print_Board is
      begin
         Put (f"  1   2   3\n");
         for I in 1 .. 3 loop
            Put (f"{I} ");

            for J in 1 .. 3 loop
               Game_Board (I)(J).Print;

               if J < 3 then
                  Put ("|");
               end if;
            end loop;

            if I < 3 then
               Put (f"\n ---+---+---\n");
            else
               Put (f"\n");
            end if;
         end loop;
      end Print_Board;

      function Check_Winner (Player : Cell'Class) return Boolean is
      begin
         -- Rows, Columns
         for I in 1 .. 3 loop
            if (Game_Board (I)(1)'Tag = Player'Tag
               and then Game_Board (I)(2)'Tag = Player'Tag
               and then Game_Board (I)(3)'Tag = Player'Tag)
            or else (Game_Board (1)(I)'Tag = Player'Tag
                     and then Game_Board (2)(I)'Tag = Player'Tag
                     and then Game_Board (3)(I)'Tag = Player'Tag)
            then
               return True;
            end if;
         end loop;

         -- Diagonals
         if (Game_Board (1)(1)'Tag = Player'Tag
            and then Game_Board (2)(2)'Tag = Player'Tag
            and then Game_Board (3)(3)'Tag = Player'Tag)
         or else (Game_Board (1)(3)'Tag = Player'Tag
                  and then Game_Board (2)(2)'Tag = Player'Tag
                  and then Game_Board (3)(1)'Tag = Player'Tag)
         then
            return True;
         end if;

         return False;
      end Check_Winner;

      function Board_Full return Boolean is
      begin
         return
            (for all I in 1 .. 3 =>
               (for all J in 1 .. 3 => Game_Board (I)(J) not in Empty_Cell'Class);
      end Board_Full;

      procedure Get_Move (Player : Cell'Class; Row, Col : out Integer) is
      begin
         loop
            Put (f"Player {Player.Symbol}");
            Put (f", enter your move (row and column): \n");
            Get (Row);
            Get (Col);
            if Row in 1 .. 3 and Col in 1 .. 3 and Game_Board (Row)(Col) in Empty_Cell
            then
               return;
            else
               Put (f"Invalid move. Try again.\n");
            end if;
         end loop;
      end Get_Move;

   begin
      Put (f"Welcome to Tic Tac Toe!\n");

      Row, Col : Integer;
      Current_Player : Cell'Class := X;

      loop
         Print_Board;
         Get_Move (Current_Player, Row, Col);
         Game_Board (Row)(Col) := Current_Player;

         if Check_Winner (Current_Player) then
            Print_Board;
            Put (f"Player {Player.Symbol} wins!\n");

            return;
         elsif Board_Full then
            Print_Board;
            Put (f"It's a draw!\n");

            return;
         else
            if Current_Player in X_Cell then
               Current_Player := O;
            else
               Current_Player := X;
            end if;
         end if;
      end loop;
   finally
      Put (f"Game Over. Thank you for playing!\n");
   end Process;

end Tic_Tac_Toe;

Feature 1

New Controlled Interface

Look at the definition of the Cell tagged type. The first aspect here is new, Finalizable => (Initialize => Init). This is the new way to introduce controlled types. With this aspect, you can specify Initialize, Adjust and Finalize without needing to derive from Ada.Controlled.Finalization. There's more hidden behind that change. First, this can be specified lower in the derivation chain, allowing you to introduce these subprograms even if the root type of the derivation doesn't. It can even be associated with a non-tagged record type, enabling finalization control without the overhead of tagged types.

Speaking of overhead, the default finalization model enabled by this aspect is a lighter version than the one specified by Ada, in particular with regard to behavior in the presence of exception or partition termination. The Ada semantics led to very significant run-time performance hits, and prevented use of this feature altogether in small run-time environments, such as bare metal. This new model is much more efficient and can be used with all run-times.

Note that in this example, we only specify Initialize, but Adjust and Finalize can be specified in the same way.

Feature 2

Maximum Size for Tagged Types Hierarchies

This one has an impact throughout the entire game. It's introduced by a new aspect on the Cell tagged type, Size'Class => 128. Being able to specify the size of a type is not new - but this specifies the size of all of the objects in the class hierarchy. In other words, type extensions here cannot be larger than 128 bits.

Introducing this aspect means that we can now create mutable class-wide objects (a bit like mutable discriminated types). Look at Current_Player : Cell'Class := X; for example. This is a declaration of a class wide type of Cell. But later in the code, we can write Current_Player := O;, modifying the type of Current_Player.

This can even be used for components of arrays and records. Since the compiler knows the maximum size of the objects, it can reserve this size. For example, on the array declaration type Board_Column is array (Integer range 1 .. <>) of Cell'Class;, we can now have an array that contains any type from the hierarchy of Cell.

Prior to this change, this would have been doable through access types and allocators, leading to not only a footprint in terms of code and runtime, but also requiring additional effort in avoiding the various problems with pointers (memory leaks, etc.). In this case, as the maximum size of my object is small, I can use this OOP design completely free of dynamic memory.


Feature 3

Super View

Look at the implementation of the Init procedure. We'd like the overriding declarations to extend the base versions, a bit like constructors (which incidentally are another improvement in the pipeline, but we're not there yet). So we'd like to have an easy way for the overridden Init subprogram to call the parent Init.

This can be done through the new 'Super attribute, for example:

overriding
procedure Init (C : in out X_Cell) is
   begin
      C'Super.Init;

This is equivalent to what would have been written before:

overriding
procedure Init (C : in out X_Cell) is
   begin
      Cell (C).Init;

Having an abstraction to denote the parent root type leads to much stronger and much more readable code. In particular, the intent here is clear, and resilient to potential refactorings that would lead to modifications in the parent hierarchy - for example if I introduce a type between Cell and X_Cell.

Feature 4

Fixed Lower Bound

You may notice that the declaration type Board_Column is array (Integer range 1 .. <>) of Cell'Class; is peculiar. It's a so-called fixed lower bound array, a new form of unconstrained array where only the upper bound is variable, the lower bound being fixed. This has two significant advantages. First, a variable lower bound introduces additional memory load and computations to retrieve an object at a given index, this form is more efficient and generates less code. On top of that, there's a known issue with Ada where developers mistakenly assume the lower bound of array parameters (e.g., you receive a String and expect the first index to be one). This form also fixes this issue. In a lot of respects, this should become the default form of unconstrained arrays.

When declaring instances or variables of such arrays, the provided lower bound should always be one. In the case of operations such as the assignment of slices, bounds will automatically slide to the necessary value.

Arguably, in this specific example, we could have gone for a 2-dimensional fixed size array anyway, but lower-bound constrained arrays can have significant advantages in real-life examples.

Feature 5

Formatted Strings

A lot of the strings in the Put calls now look like Put (f"some text\n");. The f prefix introduces the formatted string concept, which allows control characters similar to many other languages, e.g. \n introduces a new line.

You might also have noticed a line showing a more powerful usage of this capability, Put (f"{I} ");. The curly brackets allow us to introduce an arbitrary Ada expression (here it's just a reference to the I variable), take its image, and concatenate it automatically in the string.

Feature 6

Object Declaration in Sequence of Statements

In the body of Process, you might have noticed some declarations that seem misplaced:


begin
      Put (f"Welcome to Tic Tac Toe!\n");

      Row, Col : Integer;
      Current_Player : Cell'Class := X;

With the GNAT extensions, object declarations can now be placed in sequences of statements without the need to introduce a declare block. Arguably, in this specific case, it would have been just as easy to move them to the declaration section. Generally speaking though, this simplifies the declaration of variables closer to their place of usage - in particular in control flow structures, whereas the visual overhead of nested blocks may discourage this use.

Feature 7

Finally

Finally, at the very end of the code, you might have noticed the introduction of the ... well, finally section. This section introduces a sequence of statements that will be executed upon exit of the subprogram, regardless of where this exit is coming from, whether that be a return statement or an exception raised. This is very helpful, for example, to provide a systematic cleanup section.

Where we're going next

As you can see, the extensions introduced by GNAT to the Ada programming language are a mix of unique capabilities for Ada (such as fixed size hierarchies of classes) and inclusion of features that other languages have provided or proposed in the past, and are deemed useful to Ada users. We have many other capabilities of both kinds in the stove (such as an overhaul of object orientation and generics, or improvements in contract-based programming). We'll continue sharing these here as they become available in upcoming versions of the compiler.

These enhancements are being pushed to the GNAT compilers that AdaCore commits to the common tree at the FSF. While this is not complete yet, you can see the status of the implementation by compiling your own GCC on head - or you can wait for the next official GCC release. If you're a GNAT Pro customer, the easiest thing to do is to open a ticket asking for the release of a wavefront with these new capabilities. We're interested in feedback!

Erratum - a previous version of this blog post was indicating that the changes were already contributed to the FSF - we clarified the fact that this work is only partial at this stage.


Posted in #tictactoe    #Ada   

About Quentin Ochem

Quentin Ochem

Quentin Ochem is the Chief Product and Revenue Officer at AdaCore, overseeing marketing, sales, and product management. His involvement with AdaCore began in 2002 during his school years, officially joining in 2005 to work on IDE and cross-language bindings. Quentin has a background in software engineering, particularly in high-integrity domains like avionics and defense. His roles expanded to include training and technical sales, leading him to build the technical sales department and global product management in the US. In 2021, he stepped into his current role, steering the company’s strategic initiatives.

Quentin holds a master's degree in Computer Engineering from Polytech Marseille, awarded in 2005.