🖥️ Ada Programming: Files, Arrays, and Memory Management

Andrew J. Pounds, Ph.D.
Departments of Chemistry and Computer Science
Mercer University

🎯 Learning Objectives

After completing this exercise, you will be able to:

1 Command-Line Arguments

Command-line arguments allow users to pass information to a program when it is executed. In Ada, command-line arguments are accessed using the Ada.Command_Line package.

Basic Command-Line Argument Access

Ada provides functions in Ada.Command_Line to access command-line arguments:

Function Description
Argument_Count Returns the number of arguments passed (excluding the program name)
Argument(n) Returns the nth argument as a string (1-based indexing)
Command_Name Returns the name of the program
with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Command_Line;    use Ada.Command_Line;

procedure Cmd_Args is
begin
   Put_Line("Number of arguments: " & Integer'Image(Argument_Count));
   Put_Line("Program name: " & Command_Name);

   for I in 1 .. Argument_Count loop
      Put_Line("Argument(" & Integer'Image(I) & ") = " & Argument(I));
   end loop;
end Cmd_Args;

Compile and run with arguments:

gnatmake cmd_args.adb
./cmd_args hello world 123
Number of arguments: 3 Program name: ./cmd_args Argument( 1) = hello Argument( 2) = world Argument( 3) = 123
Unlike Pascal or C, Ada's Argument function uses 1-based indexing. Argument(1) is the first user-provided argument. The program name is accessed separately via Command_Name.

Processing Command-Line Arguments

Here's an example that expects a filename as a command-line argument:

with Ada.Text_IO;           use Ada.Text_IO;
with Ada.Command_Line;      use Ada.Command_Line;
with Ada.Integer_Text_IO;   use Ada.Integer_Text_IO;

procedure Process_Args is
   Count : Integer;
begin
   -- Check if correct number of arguments provided
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <filename> [count]");
      Set_Exit_Status(Failure);
      return;
   end if;

   Put_Line("Processing file: " & Argument(1));

   -- If a second argument is provided, treat it as a number
   if Argument_Count >= 2 then
      Count := Integer'Value(Argument(2));
      Put("Count parameter: ");
      Put(Count, Width => 0);
      New_Line;
   end if;
end Process_Args;
The Set_Exit_Status procedure sets the program's exit code. Failure is a predefined constant (typically 1), and Success is 0. Using return in the main procedure exits the program.

2 Reading from Files

Ada provides robust file handling capabilities through the Ada.Text_IO package. File operations follow a specific pattern: declare, open, read/write, close.

Opening and Reading a File

The basic approach uses Open, Get_Line, and Close:

with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Command_Line;   use Ada.Command_Line;

procedure Read_File_Demo is
   F    : File_Type;
   Line : String(1..1024);
   Last : Natural;
begin
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   -- Open existing file for reading
   Open(File => F,
        Mode => In_File,
        Name => Argument(1));

   -- Read line by line until end of file
   while not End_Of_File(F) loop
      Get_Line(F, Line, Last);
      Put_Line(Line(1..Last));
   end loop;

   -- Close the file
   Close(F);
end Read_File_Demo;

File Operation Procedures

Procedure/Function Description
Open(F, Mode, Name) Opens an existing file with specified mode
Create(F, Mode, Name) Creates a new file (overwrites if exists)
Close(F) Closes the file
Get_Line(F, Str, Last) Reads a line; Last indicates actual length read
Put_Line(F, Str) Writes string to file followed by newline
End_Of_File(F) Returns True if at end of file
Is_Open(F) Returns True if file is currently open

File Modes

Mode Description
In_File Open for reading only
Out_File Open for writing (creates/truncates)
Append_File Open for appending (writes to end)

Error Handling with Exception Handling

Ada uses exceptions for error handling, which is more robust than checking return codes:

with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Command_Line;   use Ada.Command_Line;

procedure Safe_File_Read is
   F        : File_Type;
   Line     : String(1..1024);
   Last     : Natural;
   Filename : String(1..256);
   Fn_Last  : Natural;
begin
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   -- Copy argument to fixed string
   Fn_Last := Argument(1)'Length;
   Filename(1..Fn_Last) := Argument(1);

   begin
      Open(File => F, Mode => In_File, Name => Filename(1..Fn_Last));
   exception
      when Name_Error =>
         Put_Line("Error: File '" & Filename(1..Fn_Last) & "' not found.");
         Set_Exit_Status(Failure);
         return;
      when Use_Error =>
         Put_Line("Error: Cannot open file '" & Filename(1..Fn_Last) & "'.");
         Set_Exit_Status(Failure);
         return;
   end;

   Put_Line("File contents:");
   while not End_Of_File(F) loop
      Get_Line(F, Line, Last);
      Put_Line(Line(1..Last));
   end loop;

   Close(F);
end Safe_File_Read;
Ada raises Name_Error when a file doesn't exist and Use_Error when a file cannot be opened for other reasons (permissions, etc.). Exception handling provides clean, structured error recovery.

Using Ada.Directories (Modern Approach)

The Ada.Directories package provides a way to check if a file exists before opening:

with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Command_Line;   use Ada.Command_Line;
with Ada.Directories;    use Ada.Directories;

procedure Check_File_Exists is
   F    : File_Type;
   Line : String(1..1024);
   Last : Natural;
begin
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   -- Check if file exists before opening
   if not Exists(Argument(1)) then
      Put_Line("Error: File '" & Argument(1) & "' not found.");
      Set_Exit_Status(Failure);
      return;
   end if;

   Open(F, In_File, Argument(1));

   while not End_Of_File(F) loop
      Get_Line(F, Line, Last);
      Put_Line(Line(1..Last));
   end loop;

   Close(F);
end Check_File_Exists;

3 Tokenizing Strings

Tokenization is the process of breaking a string into smaller pieces (tokens) based on delimiters. Ada provides several packages for string manipulation, including Ada.Strings.Fixed and Ada.Strings.Unbounded.

String Types in Ada

Type Description Package
String Fixed-length string (must know size at declaration) Built-in
Unbounded_String Variable-length string (can grow/shrink) Ada.Strings.Unbounded
Bounded_String Variable-length up to a maximum Ada.Strings.Bounded

Basic String Functions

Function Description Package
Index(Str, Pattern) Returns position of pattern (0 if not found) Ada.Strings.Fixed
Str(First..Last) Slice: extracts substring Built-in
Str'Length Returns the length of the string Built-in attribute
Trim(Str, Side) Removes whitespace from specified side(s) Ada.Strings.Fixed
To_Unbounded_String(Str) Converts fixed string to unbounded Ada.Strings.Unbounded
To_String(UStr) Converts unbounded string to fixed Ada.Strings.Unbounded

Manual Tokenization with Unbounded Strings

Here's how to split a string using a delimiter with unbounded strings:

with Ada.Text_IO;              use Ada.Text_IO;
with Ada.Strings.Unbounded;    use Ada.Strings.Unbounded;
with Ada.Strings;              use Ada.Strings;

procedure Tokenize_Demo is
   Sentence    : Unbounded_String;
   Token       : Unbounded_String;
   Delim_Pos   : Natural;
   Token_Count : Natural := 0;
begin
   Sentence := To_Unbounded_String("The quick brown fox jumps");

   Put_Line("Original: " & To_String(Sentence));
   Put_Line("Tokens:");

   -- Process tokens while string is not empty
   while Length(Sentence) > 0 loop
      Delim_Pos := Index(Sentence, " ");

      if Delim_Pos > 0 then
         -- Extract token before delimiter
         Token := Unbounded_Slice(Sentence, 1, Delim_Pos - 1);
         -- Remove token and delimiter from string
         Delete(Sentence, 1, Delim_Pos);
      else
         -- No more delimiters - rest of string is last token
         Token := Sentence;
         Sentence := Null_Unbounded_String;
      end if;

      -- Trim whitespace
      Token := Trim(Token, Both);

      if Length(Token) > 0 then
         Token_Count := Token_Count + 1;
         Put_Line("  Token" & Natural'Image(Token_Count) &
                  ": " & To_String(Token));
      end if;
   end loop;

   Put_Line("Total tokens:" & Natural'Image(Token_Count));
end Tokenize_Demo;
Original: The quick brown fox jumps Tokens: Token 1: The Token 2: quick Token 3: brown Token 4: fox Token 5: jumps Total tokens: 5

Creating a Reusable Tokenizer Function

It's helpful to create a function that extracts the next token from a string:

with Ada.Text_IO;              use Ada.Text_IO;
with Ada.Strings.Unbounded;    use Ada.Strings.Unbounded;
with Ada.Strings;              use Ada.Strings;

procedure Tokenizer_Function is

   -- Extracts and returns the next token, modifying the source string
   procedure Get_Next_Token
     (Source    : in out Unbounded_String;
      Delimiter : in     Character;
      Token     :    out Unbounded_String)
   is
      Delim_Pos : Natural;
      Delim_Str : constant String := (1 => Delimiter);
   begin
      Source := Trim(Source, Both);

      if Length(Source) = 0 then
         Token := Null_Unbounded_String;
         return;
      end if;

      Delim_Pos := Index(Source, Delim_Str);

      if Delim_Pos > 0 then
         Token := Unbounded_Slice(Source, 1, Delim_Pos - 1);
         Delete(Source, 1, Delim_Pos);
      else
         Token := Source;
         Source := Null_Unbounded_String;
      end if;

      Token := Trim(Token, Both);
   end Get_Next_Token;

   CSV_Line : Unbounded_String;
   Field    : Unbounded_String;

begin
   CSV_Line := To_Unbounded_String("Alice,25,Computer Science,3.85");

   Put_Line("Parsing CSV: " & To_String(CSV_Line));
   New_Line;

   Get_Next_Token(CSV_Line, ',', Field);
   Put_Line("Name: " & To_String(Field));

   Get_Next_Token(CSV_Line, ',', Field);
   Put_Line("Age: " & To_String(Field));

   Get_Next_Token(CSV_Line, ',', Field);
   Put_Line("Major: " & To_String(Field));

   Get_Next_Token(CSV_Line, ',', Field);
   Put_Line("GPA: " & To_String(Field));
end Tokenizer_Function;
Parsing CSV: Alice,25,Computer Science,3.85 Name: Alice Age: 25 Major: Computer Science GPA: 3.85

Tokenizing File Contents

Combining file reading with tokenization:

with Ada.Text_IO;              use Ada.Text_IO;
with Ada.Command_Line;         use Ada.Command_Line;
with Ada.Directories;          use Ada.Directories;
with Ada.Strings.Unbounded;    use Ada.Strings.Unbounded;
with Ada.Strings;              use Ada.Strings;

procedure Tokenize_File is

   procedure Get_Next_Token
     (Source    : in out Unbounded_String;
      Delimiter : in     Character;
      Token     :    out Unbounded_String)
   is
      Delim_Pos : Natural;
      Delim_Str : constant String := (1 => Delimiter);
   begin
      Source := Trim(Source, Both);
      if Length(Source) = 0 then
         Token := Null_Unbounded_String;
         return;
      end if;
      Delim_Pos := Index(Source, Delim_Str);
      if Delim_Pos > 0 then
         Token := Unbounded_Slice(Source, 1, Delim_Pos - 1);
         Delete(Source, 1, Delim_Pos);
      else
         Token := Source;
         Source := Null_Unbounded_String;
      end if;
      Token := Trim(Token, Both);
   end Get_Next_Token;

   F           : File_Type;
   Line_Buffer : String(1..1024);
   Last        : Natural;
   Line        : Unbounded_String;
   Token       : Unbounded_String;
   Token_Count : Natural := 0;

begin
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   if not Exists(Argument(1)) then
      Put_Line("Error: File not found.");
      Set_Exit_Status(Failure);
      return;
   end if;

   Open(F, In_File, Argument(1));

   while not End_Of_File(F) loop
      Get_Line(F, Line_Buffer, Last);
      Line := To_Unbounded_String(Line_Buffer(1..Last));

      -- Tokenize each line on spaces
      while Length(Line) > 0 loop
         Get_Next_Token(Line, ' ', Token);
         if Length(Token) > 0 then
            Token_Count := Token_Count + 1;
         end if;
      end loop;
   end loop;

   Close(F);
   Put_Line("Total tokens in file:" & Natural'Image(Token_Count));
end Tokenize_File;
When tokenizing on whitespace, you may encounter multiple consecutive spaces. The Trim function helps clean up the string, and checking for empty tokens prevents counting blank entries.

4 Arrays in Ada

Ada supports both constrained arrays (fixed size, determined at compile time or runtime) and unconstrained array types (flexible bounds). Ada also provides powerful array attributes for safe iteration.

Constrained Arrays

Constrained arrays have bounds that are fixed when declared:

with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Static_Array_Demo is
   -- Array with indices 1 to 5
   Numbers : array (1 .. 5) of Integer;

   -- Array with indices 0 to 4
   Grades : array (0 .. 4) of Float;

   -- Named array type with initialization
   type Day_Names is array (1 .. 7) of String(1..3);
   Days : constant Day_Names := ("Mon", "Tue", "Wed", "Thu",
                                  "Fri", "Sat", "Sun");
begin
   -- Assign values
   for I in Numbers'Range loop
      Numbers(I) := I * 10;
   end loop;

   -- Access values
   Put_Line("Numbers:");
   for I in Numbers'Range loop
      Put("  Numbers(");
      Put(I, Width => 0);
      Put(") = ");
      Put(Numbers(I), Width => 0);
      New_Line;
   end loop;

   New_Line;
   Put_Line("Days of the week:");
   for I in Days'Range loop
      Put_Line("  " & Days(I));
   end loop;
end Static_Array_Demo;
Ada arrays can use any discrete type as indices—integers, characters, or enumerated types. The 'Range attribute provides the valid index range for safe iteration, and 'First and 'Last give the bounds.

Unconstrained Array Types

Unconstrained array types allow you to define arrays whose bounds are specified when objects are created:

with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Unconstrained_Array_Demo is
   -- Define unconstrained array type (bounds not yet specified)
   type Int_Array is array (Positive range <>) of Integer;

   -- Create array with bounds at declaration
   Numbers : Int_Array(1..5);

   -- Function that returns array of any size
   function Create_Sequence(Size : Positive) return Int_Array is
      Result : Int_Array(1..Size);
   begin
      for I in Result'Range loop
         Result(I) := I * 10;
      end loop;
      return Result;
   end Create_Sequence;

   Seq : constant Int_Array := Create_Sequence(8);

begin
   -- Initialize Numbers
   for I in Numbers'Range loop
      Numbers(I) := I * 10;
   end loop;

   Put_Line("Numbers array (length" & Integer'Image(Numbers'Length) & "):");
   for I in Numbers'Range loop
      Put("  Numbers(");
      Put(I, Width => 0);
      Put(") = ");
      Put(Numbers(I), Width => 0);
      New_Line;
   end loop;

   New_Line;
   Put_Line("Sequence array (length" & Integer'Image(Seq'Length) & "):");
   for I in Seq'Range loop
      Put("  Seq(");
      Put(I, Width => 0);
      Put(") = ");
      Put(Seq(I), Width => 0);
      New_Line;
   end loop;
end Unconstrained_Array_Demo;

Useful Array Attributes

Attribute Description
Arr'Length Returns the number of elements in the array
Arr'First Returns the first (lowest) valid index
Arr'Last Returns the last (highest) valid index
Arr'Range Returns the range Arr'First..Arr'Last
Always use 'Range, 'First, and 'Last instead of hardcoded bounds. This prevents off-by-one errors and makes code work correctly regardless of array bounds.

Dynamic Arrays with Vectors

For truly dynamic arrays that can grow at runtime, Ada provides the Ada.Containers.Vectors package:

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Integer_Text_IO;             use Ada.Integer_Text_IO;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure Vector_Demo is
   -- Instantiate a vector package for integers
   package Int_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Integer);
   use Int_Vectors;

   Numbers : Vector;

begin
   -- Start with empty vector
   Put_Line("Initial length:" &
            Count_Type'Image(Length(Numbers)));

   -- Append elements (vector grows automatically)
   Append(Numbers, 10);
   Append(Numbers, 20);
   Append(Numbers, 30);
   Append(Numbers, 40);
   Append(Numbers, 50);

   Put_Line("After appending (length" &
            Count_Type'Image(Length(Numbers)) & "):");
   for I in Numbers.First_Index .. Numbers.Last_Index loop
      Put("  Numbers(");
      Put(I, Width => 0);
      Put(") = ");
      Put(Element(Numbers, I), Width => 0);
      New_Line;
   end loop;

   -- Add more elements
   Append(Numbers, 60);
   Append(Numbers, 70);
   Append(Numbers, 80);

   New_Line;
   Put_Line("After growing (length" &
            Count_Type'Image(Length(Numbers)) & "):");
   for E of Numbers loop
      Put("  ");
      Put(E, Width => 0);
      New_Line;
   end loop;
end Vector_Demo;
Initial length: 0 After appending (length 5): Numbers(0) = 10 Numbers(1) = 20 Numbers(2) = 30 Numbers(3) = 40 Numbers(4) = 50 After growing (length 8): 10 20 30 40 50 60 70 80
Ada.Containers.Vectors is similar to C++'s std::vector or Java's ArrayList. It automatically manages memory and resizing. The for E of Numbers syntax is Ada 2012's "for of" loop for container iteration.

Vector of Strings

A complete example using vectors for both strings and numbers:

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure String_Vector_Demo is
   -- Vector of unbounded strings
   package String_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Unbounded_String);
   use String_Vectors;

   Words : Vector;

begin
   -- Append string elements
   Append(Words, To_Unbounded_String("Hello"));
   Append(Words, To_Unbounded_String("Dynamic"));
   Append(Words, To_Unbounded_String("World"));

   Put_Line("Words:");
   for I in Words.First_Index .. Words.Last_Index loop
      Put_Line("  [" & Natural'Image(I) & "]: " &
               To_String(Element(Words, I)));
   end loop;

   -- Alternative: use "for of" loop
   New_Line;
   Put_Line("Using for-of loop:");
   for W of Words loop
      Put_Line("  " & To_String(W));
   end loop;
end String_Vector_Demo;

Storing Tokens in a Vector

A complete example that reads a file, tokenizes it, and stores tokens in a vector:

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Command_Line;                use Ada.Command_Line;
with Ada.Directories;                 use Ada.Directories;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Strings;                     use Ada.Strings;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure Tokens_To_Vector is
   package String_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Unbounded_String);
   use String_Vectors;

   procedure Get_Next_Token
     (Source    : in out Unbounded_String;
      Delimiter : in     Character;
      Token     :    out Unbounded_String)
   is
      Delim_Pos : Natural;
      Delim_Str : constant String := (1 => Delimiter);
   begin
      Source := Trim(Source, Both);
      if Length(Source) = 0 then
         Token := Null_Unbounded_String;
         return;
      end if;
      Delim_Pos := Index(Source, Delim_Str);
      if Delim_Pos > 0 then
         Token := Unbounded_Slice(Source, 1, Delim_Pos - 1);
         Delete(Source, 1, Delim_Pos);
      else
         Token := Source;
         Source := Null_Unbounded_String;
      end if;
      Token := Trim(Token, Both);
   end Get_Next_Token;

   Tokens      : Vector;
   F           : File_Type;
   Line_Buffer : String(1..1024);
   Last        : Natural;
   Line        : Unbounded_String;
   Token       : Unbounded_String;

begin
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   if not Exists(Argument(1)) then
      Put_Line("Error: File not found.");
      Set_Exit_Status(Failure);
      return;
   end if;

   Open(F, In_File, Argument(1));

   while not End_Of_File(F) loop
      Get_Line(F, Line_Buffer, Last);
      Line := To_Unbounded_String(Line_Buffer(1..Last));
      Line := Trim(Line, Both);

      while Length(Line) > 0 loop
         Get_Next_Token(Line, ' ', Token);
         if Length(Token) > 0 then
            Append(Tokens, Token);
         end if;
      end loop;
   end loop;

   Close(F);

   -- Display results
   Put_Line("=== File Token Analysis ===");
   Put_Line("Total tokens:" &
            Count_Type'Image(Length(Tokens)));

   if not Is_Empty(Tokens) then
      Put_Line("First token: " &
               To_String(First_Element(Tokens)));
      Put_Line("Last token: " &
               To_String(Last_Element(Tokens)));
   end if;

   New_Line;
   Put_Line("Tokens longer than 5 characters:");
   for I in Tokens.First_Index .. Tokens.Last_Index loop
      if Ada.Strings.Unbounded.Length(Element(Tokens, I)) > 5 then
         Put_Line("  [" & Natural'Image(I) & "]: " &
                  To_String(Element(Tokens, I)) &
                  " (length:" &
                  Natural'Image(Ada.Strings.Unbounded.Length(
                     Element(Tokens, I))) & ")");
      end if;
   end loop;
end Tokens_To_Vector;

5 Memory Management in Ada

Ada provides strong memory safety guarantees. Unlike garbage-collected languages, Ada gives you control over memory management while providing compile-time checks to prevent many common errors.

Automatic vs. Manual Memory Management

Ada uses different strategies depending on the type of data:

Data Type Memory Management Notes
Local variables Automatic Allocated on stack, freed when scope ends
Unbounded_String Automatic (managed) Implementation handles memory
Containers (Vectors, etc.) Automatic (managed) Freed when container goes out of scope
Access types (new) Manual or Controlled Must use Unchecked_Deallocation or Controlled types
Controlled types Automatic (destructor) Finalize called automatically

Automatic Memory Management with Containers

Ada's standard containers handle memory automatically:

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure Auto_Memory_Demo is
   package String_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Unbounded_String);
   use String_Vectors;

   procedure Create_Data is
      Data : Vector;
   begin
      -- Memory is allocated automatically
      for I in 1 .. 1000 loop
         Append(Data, To_Unbounded_String("Item" & Integer'Image(I)));
      end loop;

      Put_Line("Created" &
               Count_Type'Image(Length(Data)) &
               " items inside procedure");

      -- When this procedure returns, Data goes out of scope
      -- Memory is automatically freed (destructor called)
   end Create_Data;

begin
   Put_Line("=== Automatic Memory Management Demo ===");

   for I in 1 .. 5 loop
      New_Line;
      Put_Line("Iteration" & Integer'Image(I));
      Create_Data;
      -- Memory from previous call has been freed
   end loop;

   New_Line;
   Put_Line("Program completed - all memory cleaned up automatically");
end Auto_Memory_Demo;
Ada containers use controlled types internally, which means their Finalize procedure is called automatically when they go out of scope. This provides deterministic memory management—memory is freed immediately when the scope ends, not at some later time chosen by a garbage collector.

Manual Memory Management with Access Types

When using access types (pointers) with new, you can manually deallocate memory:

with Ada.Text_IO;           use Ada.Text_IO;
with Ada.Float_Text_IO;     use Ada.Float_Text_IO;
with Ada.Unchecked_Deallocation;

procedure Manual_Memory_Demo is
   type Student is record
      Name        : String(1..50);
      Name_Length : Natural;
      Age         : Integer;
      GPA         : Float;
   end record;

   type Student_Access is access Student;

   -- Instantiate deallocation procedure
   procedure Free is new Ada.Unchecked_Deallocation
     (Object => Student,
      Name   => Student_Access);

   S : Student_Access;

begin
   Put_Line("=== Manual Memory Management Demo ===");

   -- Allocate memory for the record
   S := new Student;
   Put_Line("Memory allocated with 'new'");

   -- Use the allocated memory
   S.Name(1..5) := "Alice";
   S.Name_Length := 5;
   S.Age := 20;
   S.GPA := 3.85;

   Put_Line("Student: " & S.Name(1..S.Name_Length));
   Put_Line("Age:" & Integer'Image(S.Age));
   Put("GPA: ");
   Put(S.GPA, Fore => 1, Aft => 2, Exp => 0);
   New_Line;

   -- IMPORTANT: Free the memory when done
   Free(S);
   Put_Line("Memory freed with Free()");

   -- S is now null (Free sets it to null)
   if S = null then
      Put_Line("Pointer is now null (safe)");
   end if;
end Manual_Memory_Demo;
When using new to allocate memory, you must instantiate Ada.Unchecked_Deallocation to free it. The word "Unchecked" reminds you that you're taking responsibility for correct deallocation. Forgetting to free memory causes memory leaks.

Comparison: Ada vs. Garbage-Collected Languages

Feature Ada Garbage-Collected (Java, Python)
Containers/Collections Automatic (Controlled types) Automatic (GC)
Strings (Unbounded) Automatic (managed) Automatic (GC)
Pointer/access allocation Manual (Unchecked_Deallocation) Automatic (GC)
When memory is freed Immediately (deterministic) When GC runs (unpredictable)
Memory leaks possible Yes (if Free forgotten) Rare (GC prevents most)
Dangling pointer protection Free sets to null GC prevents
Performance overhead Minimal GC pauses possible

Best Practices for Memory Management

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;
with Ada.Unchecked_Deallocation;

procedure Memory_Best_Practices is
   -- For dynamic data, prefer containers over raw access types
   package Int_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Integer);
   use Int_Vectors;

   -- If you must use access types, encapsulate allocation/deallocation
   type Data_Record is record
      Values : Vector;
      Name   : Unbounded_String;
   end record;

   type Data_Access is access Data_Record;

   procedure Free is new Ada.Unchecked_Deallocation
     (Object => Data_Record,
      Name   => Data_Access);

   Data : Data_Access := null;  -- Initialize to null

begin
   Put_Line("=== Memory Best Practices ===");

   begin
      -- Allocate memory
      Data := new Data_Record;

      -- Use the memory
      Data.Name := To_Unbounded_String("Test Data");
      Append(Data.Values, 100);
      Append(Data.Values, 200);

      Put_Line("Data name: " & To_String(Data.Name));
      Put_Line("First value:" & Integer'Image(First_Element(Data.Values)));

   exception
      when others =>
         -- Clean up on error
         if Data /= null then
            Free(Data);
         end if;
         raise;  -- Re-raise the exception
   end;

   -- Normal cleanup
   if Data /= null then
      Free(Data);
      -- Data is now null
   end if;

   Put_Line("Memory properly cleaned up");
end Memory_Best_Practices;
Best Practice: Prefer Ada containers (Vectors, Maps, etc.) over manual memory management with access types. Containers handle memory automatically and are safer. Only use new and Unchecked_Deallocation when you have a specific reason that containers can't satisfy.

Practical Example: Processing Large Data

This example shows how containers handle memory automatically when processing files:

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Command_Line;                use Ada.Command_Line;
with Ada.Directories;                 use Ada.Directories;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Strings;                     use Ada.Strings;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure Process_Large_Data is
   package String_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Unbounded_String);
   use String_Vectors;

   procedure Get_Next_Token
     (Source    : in Out Unbounded_String;
      Delimiter : in     Character;
      Token     :    out Unbounded_String)
   is
      Delim_Pos : Natural;
      Delim_Str : constant String := (1 => Delimiter);
   begin
      Source := Trim(Source, Both);
      if Length(Source) = 0 then
         Token := Null_Unbounded_String;
         return;
      end if;
      Delim_Pos := Index(Source, Delim_Str);
      if Delim_Pos > 0 then
         Token := Unbounded_Slice(Source, 1, Delim_Pos - 1);
         Delete(Source, 1, Delim_Pos);
      else
         Token := Source;
         Source := Null_Unbounded_String;
      end if;
      Token := Trim(Token, Both);
   end Get_Next_Token;

   procedure Analyze_File(Filename : String) is
      F           : File_Type;
      Line_Buffer : String(1..4096);
      Last        : Natural;
      Line        : Unbounded_String;
      Token       : Unbounded_String;
      Tokens      : Vector;
      Long_Words  : Vector;
   begin
      Put_Line("Analyzing: " & Filename);

      -- Vectors allocated automatically
      Open(F, In_File, Filename);

      while not End_Of_File(F) loop
         Get_Line(F, Line_Buffer, Last);
         Line := To_Unbounded_String(Line_Buffer(1..Last));
         Line := Trim(Line, Both);

         while Length(Line) > 0 loop
            Get_Next_Token(Line, ' ', Token);
            if Length(Token) > 0 then
               Append(Tokens, Token);
               if Length(Token) > 6 then
                  Append(Long_Words, Token);
               end if;
            end if;
         end loop;
      end loop;

      Close(F);

      Put_Line("  Tokens found:" & Count_Type'Image(Length(Tokens)));
      Put_Line("  Long words:" & Count_Type'Image(Length(Long_Words)));

      -- When this procedure returns:
      -- - Tokens vector is finalized -> memory freed
      -- - Long_Words vector is finalized -> memory freed
      -- - All Unbounded_Strings in vectors are freed
      -- No manual cleanup needed!
   end Analyze_File;

begin
   if Argument_Count < 1 then
      Put_Line("Usage: " & Command_Name & " <file1> [file2] [file3] ...");
      Set_Exit_Status(Failure);
      return;
   end if;

   Put_Line("=== Multi-File Analyzer ===");
   New_Line;

   for I in 1 .. Argument_Count loop
      if Exists(Argument(I)) then
         Analyze_File(Argument(I));
         New_Line;
      else
         Put_Line("Skipping (not found): " & Argument(I));
      end if;
   end loop;

   Put_Line("Done! All temporary memory has been reclaimed.");
end Process_Large_Data;
Key Insight: For most Ada programs using containers and Unbounded_String, you don't need to worry about memory management—Ada handles it automatically through controlled types. Manual memory management is only needed when using access types with new.

6 Complete Example: Word Frequency Analyzer

This comprehensive example combines all the concepts: command-line arguments, file reading, tokenization, containers, and demonstrates Ada's memory management:

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Command_Line;                use Ada.Command_Line;
with Ada.Directories;                 use Ada.Directories;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Strings;                     use Ada.Strings;
with Ada.Characters.Handling;         use Ada.Characters.Handling;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure Word_Frequency_Analyzer is
   package String_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Unbounded_String);
   use String_Vectors;

   package Int_Vectors is new Ada.Containers.Vectors
     (Index_Type   => Natural,
      Element_Type => Integer);
   use Int_Vectors;

   -- Get next token from string
   procedure Get_Next_Token
     (Source    : in out Unbounded_String;
      Delimiter : in     Character;
      Token     :    out Unbounded_String)
   is
      Delim_Pos : Natural;
      Delim_Str : constant String := (1 => Delimiter);
   begin
      Source := Trim(Source, Both);
      if Length(Source) = 0 then
         Token := Null_Unbounded_String;
         return;
      end if;
      Delim_Pos := Index(Source, Delim_Str);
      if Delim_Pos > 0 then
         Token := Unbounded_Slice(Source, 1, Delim_Pos - 1);
         Delete(Source, 1, Delim_Pos);
      else
         Token := Source;
         Source := Null_Unbounded_String;
      end if;
      Token := Trim(Token, Both);
   end Get_Next_Token;

   -- Check if character is punctuation
   function Is_Punctuation(C : Character) return Boolean is
   begin
      return C = '.' or else C = ',' or else C = '!' or else
             C = '?' or else C = ';' or else C = ':' or else
             C = '"' or else C = ''' or else C = '(' or else
             C = ')' or else C = '-';
   end Is_Punctuation;

   -- Remove punctuation from beginning and end of a word
   function Strip_Punctuation(S : Unbounded_String) return Unbounded_String is
      Result : Unbounded_String := S;
   begin
      while Length(Result) > 0
        and then Is_Punctuation(Element(Result, 1))
      loop
         Delete(Result, 1, 1);
      end loop;

      while Length(Result) > 0
        and then Is_Punctuation(Element(Result, Length(Result)))
      loop
         Delete(Result, Length(Result), Length(Result));
      end loop;

      return Result;
   end Strip_Punctuation;

   -- Variables
   F           : File_Type;
   Line_Buffer : String(1..4096);
   Last        : Natural;
   Line        : Unbounded_String;
   Token       : Unbounded_String;
   Word        : Unbounded_String;
   Filename    : Unbounded_String;

   -- Dynamic containers for storing data
   Tokens       : String_Vectors.Vector;
   Unique_Words : String_Vectors.Vector;
   Word_Counts  : Int_Vectors.Vector;

   Found       : Boolean;
   Max_Count   : Integer := 0;
   Most_Common : Unbounded_String;

begin
   -- Check command-line arguments
   if Argument_Count < 1 then
      Put_Line("Word Frequency Analyzer");
      Put_Line("Usage: " & Command_Name & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   Filename := To_Unbounded_String(Argument(1));

   -- Check if file exists
   if not Exists(To_String(Filename)) then
      Put_Line("Error: File '" & To_String(Filename) & "' not found.");
      Set_Exit_Status(Failure);
      return;
   end if;

   -- Read and tokenize the file
   Open(F, In_File, To_String(Filename));

   while not End_Of_File(F) loop
      Get_Line(F, Line_Buffer, Last);
      Line := To_Unbounded_String(To_Lower(Line_Buffer(1..Last)));
      Line := Trim(Line, Both);

      while Length(Line) > 0 loop
         Get_Next_Token(Line, ' ', Token);
         if Length(Token) > 0 then
            String_Vectors.Append(Tokens, Token);
         end if;
      end loop;
   end loop;

   Close(F);

   -- Count word frequencies
   for I in Tokens.First_Index .. Tokens.Last_Index loop
      Word := Strip_Punctuation(String_Vectors.Element(Tokens, I));

      if Length(Word) > 0 then
         -- Check if word already exists
         Found := False;
         for J in Unique_Words.First_Index .. Unique_Words.Last_Index loop
            if String_Vectors.Element(Unique_Words, J) = Word then
               Int_Vectors.Replace_Element
                 (Word_Counts, J,
                  Int_Vectors.Element(Word_Counts, J) + 1);
               Found := True;
               exit;
            end if;
         end loop;

         -- Add new word if not found
         if not Found then
            String_Vectors.Append(Unique_Words, Word);
            Int_Vectors.Append(Word_Counts, 1);
         end if;
      end if;
   end loop;

   -- Display results
   Put_Line("=== Word Frequency Analysis ===");
   Put_Line("File: " & To_String(Filename));
   Put_Line("Total tokens:" &
            Count_Type'Image(String_Vectors.Length(Tokens)));
   Put_Line("Unique words:" &
            Count_Type'Image(String_Vectors.Length(Unique_Words)));
   New_Line;

   -- Display words with count > 1
   Put_Line("Words appearing more than once:");
   Put_Line("--------------------------------");
   for I in Unique_Words.First_Index .. Unique_Words.Last_Index loop
      if Int_Vectors.Element(Word_Counts, I) > 1 then
         declare
            W : constant String :=
                To_String(String_Vectors.Element(Unique_Words, I));
            C : constant Integer := Int_Vectors.Element(Word_Counts, I);
            Pad_Len : constant Natural :=
                      (if W'Length < 15 then 15 - W'Length else 1);
            Padding : constant String(1..Pad_Len) := (others => ' ');
         begin
            Put_Line("  " & W & Padding & ":" & Integer'Image(C));
         end;
      end if;
   end loop;

   -- Find most common word
   for I in Unique_Words.First_Index .. Unique_Words.Last_Index loop
      if Int_Vectors.Element(Word_Counts, I) > Max_Count then
         Max_Count := Int_Vectors.Element(Word_Counts, I);
         Most_Common := String_Vectors.Element(Unique_Words, I);
      end if;
   end loop;

   New_Line;
   Put_Line("Most common word: '" & To_String(Most_Common) &
            "' (" & Integer'Image(Max_Count) & " occurrences)");

   -- Memory note: All containers (Tokens, Unique_Words, Word_Counts)
   -- will be automatically freed when the program ends because their
   -- Finalize procedures are called. No manual cleanup needed!
end Word_Frequency_Analyzer;

7 Quick Reference

Task Ada Syntax
Get argument count Argument_Count
Get argument n Argument(n)
Get program name Command_Name
Set exit status Set_Exit_Status(Failure);
Declare file variable F : File_Type;
Open file for reading Open(F, In_File, Filename);
Read line from file Get_Line(F, Buffer, Last);
Check end of file End_Of_File(F)
Close file Close(F);
Check file exists Ada.Directories.Exists(Name)
Find substring position Index(Str, Pattern)
Extract substring Str(First..Last) or Unbounded_Slice
Delete from unbounded string Delete(Str, From, Through);
Instantiate vector package package My_Vec is new Ada.Containers.Vectors(...);
Declare vector variable V : My_Vec.Vector;
Append to vector Append(V, Element);
Get vector length Length(V)
Get vector element Element(V, Index)
Array bounds Arr'First, Arr'Last, Arr'Range
Allocate memory Ptr := new Type;
Deallocate memory Free(Ptr); (after instantiating Unchecked_Deallocation)

✏️ Programming Exercise: CSV Data Processor

Write an Ada program that processes a CSV (Comma-Separated Values) file containing student grade data. Your program should:

  1. Accept the filename as a command-line argument
    • Display a usage message if no filename is provided
    • Display an error if the file doesn't exist
  2. Read and parse the CSV file where each line has the format:
    StudentName,Score1,Score2,Score3
    Example: Alice,85,92,78
  3. Store the data in vectors:
    • A vector of Unbounded_String for student names
    • A vector of Float for each student's average score
  4. Calculate and display:
    • Each student's name and average score
    • The class average (average of all student averages)
    • The highest and lowest scoring students
  5. Demonstrate understanding of memory management by adding a comment in your code explaining where memory is automatically freed
grades.csv
Alice,85,92,78
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92

Sample Output

=== Student Grade Analyzer === File: grades.csv Students loaded: 4 Student Averages: Alice : 85.00 Bob : 91.00 Charlie : 71.67 Diana : 95.00 Class Statistics: Class Average : 85.67 Highest Score : Diana (95.00) Lowest Score : Charlie (71.67)

Hints

Submission

Save your program as grade_analyzer.adb and compile with:

gnatmake grade_analyzer.adb
./grade_analyzer grades.csv

📋 Student Starter Code

Use the following template to get started. Fill in each blank marked with ______ with the appropriate code. Read the comments carefully for hints!

Instructions

  1. Save this code as grade_analyzer.adb
  2. Read through all the comments to understand the program structure
  3. Fill in each blank with the appropriate code
  4. Compile and test your program with the sample grades.csv file
-----------------------------------------------------------------------
-- CSV Grade Analyzer - Student Starter Code
-- =========================================
-- Complete the sections marked with TODO to finish this program.
--
-- This program will:
-- 1. Accept a CSV filename as a command-line argument
-- 2. Read and parse the CSV file (Name,Score1,Score2,Score3)
-- 3. Store student names and calculated averages in vectors
-- 4. Display individual averages and class statistics
--
-- Author: [Your Name]
-- Date:   [Date]
-----------------------------------------------------------------------

with Ada.Text_IO;                     use Ada.Text_IO;
with Ada.Float_Text_IO;               use Ada.Float_Text_IO;
with Ada.Command_Line;                use Ada.Command_Line;
with Ada.Directories;                 use Ada.Directories;
with Ada.Strings.Unbounded;           use Ada.Strings.Unbounded;
with Ada.Strings;                     use Ada.Strings;
with Ada.Containers.Vectors;
with Ada.Containers;                  use Ada.Containers;

procedure Grade_Analyzer is

   -----------------------------------------------------------------------
   -- PART 1: Vector Package Instantiations
   -----------------------------------------------------------------------
   -- IMPORTANT: We need separate vector packages for different types

   -- TODO: Complete the vector instantiation for Unbounded_String
   -- HINT: Index_Type should be Natural, Element_Type is the data type
   package String_Vectors is new Ada.Containers.Vectors
     (Index_Type   => ______,
      Element_Type => ______);
   use String_Vectors;

   -- TODO: Complete the vector instantiation for Float
   package Float_Vectors is new Ada.Containers.Vectors
     (Index_Type   => ______,
      Element_Type => ______);
   use Float_Vectors;

   -----------------------------------------------------------------------
   -- PART 2: Get_Next_Token Procedure
   -----------------------------------------------------------------------
   -- Extracts the next token from a string based on a delimiter.
   -- Modifies the source string by removing the extracted token.
   --
   -- Parameters:
   --   Source    - the string to extract from (modified by this procedure)
   --   Delimiter - the character to split on
   --   Token     - the extracted token (output parameter)
   --
   -- HINT: Use Index() to find the delimiter, Unbounded_Slice() to extract,
   --       and Delete() to remove the token from source.

   procedure Get_Next_Token
     (Source    : in Out Unbounded_String;
      Delimiter : in     Character;
      Token     :    out Unbounded_String)
   is
      Delim_Pos : Natural;
      Delim_Str : constant String := (1 => Delimiter);
   begin
      Source := Trim(Source, Both);

      if Length(Source) = 0 then
         Token := Null_Unbounded_String;
         return;
      end if;

      -- TODO: Find the position of the delimiter in Source
      -- HINT: Use Index(stringToSearch, patternToFind)
      Delim_Pos := ______(Source, Delim_Str);

      if Delim_Pos > 0 then
         -- TODO: Extract token (everything before the delimiter)
         -- HINT: Use Unbounded_Slice(source, startPos, endPos)
         Token := ______(Source, 1, Delim_Pos - 1);

         -- TODO: Remove the token and delimiter from source
         -- HINT: Use Delete(stringVar, fromPos, throughPos)
         ______(Source, 1, Delim_Pos);
      else
         -- No delimiter found - return entire string
         Token := Source;
         Source := Null_Unbounded_String;
      end if;

      Token := Trim(Token, Both);
   end Get_Next_Token;

   -----------------------------------------------------------------------
   -- Variable Declarations
   -----------------------------------------------------------------------

   -- File handling
   F           : File_Type;
   Line_Buffer : String(1..1024);
   Last        : Natural;
   Line        : Unbounded_String;
   Filename    : Unbounded_String;

   -- Vectors for storing student data
   Names    : String_Vectors.Vector;
   Averages : Float_Vectors.Vector;

   -- Variables for parsing and calculations
   Token                    : Unbounded_String;
   Score1, Score2, Score3   : Float;
   Student_Avg, Class_Total : Float := 0.0;
   Class_Avg                : Float;
   Highest_Avg, Lowest_Avg  : Float;
   Highest_Name             : Unbounded_String;
   Lowest_Name              : Unbounded_String;

-----------------------------------------------------------------------
-- MAIN PROGRAM
-----------------------------------------------------------------------
begin

   -----------------------------------------------------------------------
   -- PART 3: Command-Line Argument Handling
   -----------------------------------------------------------------------

   -- TODO: Check if a filename was provided
   -- HINT: Use Argument_Count to get number of arguments
   if ______ < 1 then
      Put_Line("=== Student Grade Analyzer ===");
      -- TODO: Display the program name using Command_Name
      Put_Line("Usage: " & ______ & " <filename>");
      Set_Exit_Status(Failure);
      return;
   end if;

   -- TODO: Get the filename from command-line argument #1
   -- HINT: Use Argument(n) and To_Unbounded_String()
   Filename := To_Unbounded_String(______(1));

   -- TODO: Check if file exists
   -- HINT: Use Exists() function from Ada.Directories
   if not ______(To_String(Filename)) then
      Put_Line("Error: File '" & To_String(Filename) & "' not found.");
      Set_Exit_Status(Failure);
      return;
   end if;

   -----------------------------------------------------------------------
   -- PART 4: Open File and Read Data
   -----------------------------------------------------------------------

   -- TODO: Open the file for reading
   -- HINT: Use Open(fileVar, mode, filename)
   -- Mode for reading is In_File
   ______(F, ______, To_String(Filename));

   -- TODO: Loop while not at end of file
   -- HINT: Use "while not End_Of_File(fileVar) loop"
   while not ______(F) loop
      -- TODO: Read a line from the file
      -- HINT: Use Get_Line(fileVar, buffer, lastPos)
      ______(F, Line_Buffer, Last);

      Line := To_Unbounded_String(Line_Buffer(1..Last));

      -- Skip empty lines
      if Length(Trim(Line, Both)) = 0 then
         goto Continue;
      end if;

      ----------------------------------------------------------------
      -- PART 5: Parse the CSV Line
      ----------------------------------------------------------------

      -- TODO: Extract student name (first token before comma)
      -- HINT: Call Get_Next_Token with Line, ',', and Token
      ______(Line, ',', Token);

      -- Store the name (Token now contains the student name)
      declare
         Student_Name : constant Unbounded_String := Token;
      begin
         -- TODO: Extract Score1 - get next token and convert to Float
         -- HINT: Use Float'Value(To_String(token)) to convert
         Get_Next_Token(Line, ',', Token);
         Score1 := ______'Value(To_String(Token));

         -- TODO: Extract Score2
         Get_Next_Token(Line, ',', Token);
         Score2 := ______'______(To_String(Token));

         -- TODO: Extract Score3
         Get_Next_Token(Line, ',', Token);
         Score3 := ______'______(______);

         -- TODO: Calculate the student's average
         -- Formula: (score1 + score2 + score3) / 3.0
         Student_Avg := (______ + ______ + ______) / 3.0;

         -- TODO: Append student name to Names vector
         -- HINT: Use Append(vectorVar, element) from String_Vectors
         String_Vectors.______(Names, Student_Name);

         -- TODO: Append student average to Averages vector
         Float_Vectors.______(Averages, Student_Avg);
      end;

      <<Continue>>
      null;  -- Required: a label must be followed by a statement
   end loop;

   -- TODO: Close the file
   -- HINT: Use Close(fileVar)
   ______(F);

   -----------------------------------------------------------------------
   -- PART 6: Calculate Statistics
   -----------------------------------------------------------------------

   -- Initialize statistics variables
   Class_Total := 0.0;
   Highest_Avg := 0.0;
   Lowest_Avg := 999999.0;

   -- TODO: Loop through all students to calculate statistics
   -- HINT: Use "for I in Names.First_Index .. Names.Last_Index loop"
   for I in Names.______ .. Names.______ loop

      -- Add this student's average to class total
      Class_Total := Class_Total + Float_Vectors.Element(Averages, I);

      -- TODO: Check if this is the highest average so far
      -- HINT: Compare Float_Vectors.Element(Averages, I) with Highest_Avg
      if Float_Vectors.Element(Averages, I) > ______ then
         Highest_Avg := Float_Vectors.Element(Averages, I);
         -- TODO: Save the name of the highest-scoring student
         Highest_Name := String_Vectors.______(Names, I);
      end if;

      -- TODO: Check if this is the lowest average so far
      if Float_Vectors.Element(Averages, I) < ______ then
         Lowest_Avg := Float_Vectors.Element(Averages, I);
         -- TODO: Save the name of the lowest-scoring student
         Lowest_Name := String_Vectors.______(Names, I);
      end if;
   end loop;

   -- TODO: Calculate class average
   -- Formula: classTotal / numberOfStudents
   -- HINT: Use Float(Length(Names)) to convert count to Float
   Class_Avg := Class_Total / Float(String_Vectors.______(Names));

   -----------------------------------------------------------------------
   -- PART 7: Display Results
   -----------------------------------------------------------------------

   Put_Line("=== Student Grade Analyzer ===");
   Put_Line("File: " & To_String(Filename));
   -- TODO: Display the number of students loaded
   -- HINT: Use Count_Type'Image(Length(vectorVar))
   Put_Line("Students loaded:" &
            Count_Type'Image(String_Vectors.______(Names)));
   New_Line;

   Put_Line("Student Averages:");
   for I in Names.First_Index .. Names.Last_Index loop
      declare
         Name : constant String :=
                To_String(String_Vectors.Element(Names, I));
         Avg  : constant Float := Float_Vectors.Element(Averages, I);
         Pad_Len : constant Natural :=
                   (if Name'Length < 15 then 15 - Name'Length else 1);
         Padding : constant String(1..Pad_Len) := (others => ' ');
      begin
         Put("  " & Name & Padding & ": ");
         Put(Avg, Fore => 1, Aft => 2, Exp => 0);
         New_Line;
      end;
   end loop;

   New_Line;
   Put_Line("Class Statistics:");
   Put("  Class Average  : ");
   Put(Class_Avg, Fore => 1, Aft => 2, Exp => 0);
   New_Line;
   Put("  Highest Score  : " & To_String(Highest_Name) & " (");
   Put(Highest_Avg, Fore => 1, Aft => 2, Exp => 0);
   Put_Line(")");
   Put("  Lowest Score   : " & To_String(Lowest_Name) & " (");
   Put(Lowest_Avg, Fore => 1, Aft => 2, Exp => 0);
   Put_Line(")");

   -----------------------------------------------------------------------
   -- MEMORY MANAGEMENT EXPLANATION
   -----------------------------------------------------------------------
   -- TODO: Complete this section by answering the questions
   --
   -- 1. Why don't we need to manually free the Names and Averages vectors?
   --    YOUR ANSWER: ________________________________________________
   --
   -- 2. When exactly is the memory for these vectors released?
   --    YOUR ANSWER: ________________________________________________
   --
   -- 3. How is this different from languages like Java or Python?
   --    YOUR ANSWER: ________________________________________________
   --
   -----------------------------------------------------------------------

end Grade_Analyzer;

Blank Reference Guide

Use this table to check your work. Each blank corresponds to a specific Ada concept:

Section Blank Location Concept Needed
Part 1 Index_Type => ______ Natural (standard index type)
Part 1 Element_Type => ______ (strings) Unbounded_String
Part 1 Element_Type => ______ (floats) Float
Part 2 Delim_Pos := ______ Index function
Part 2 Token := ______ Unbounded_Slice function
Part 2 ______(Source, 1, Delim_Pos) Delete procedure
Part 3 if ______ < 1 Argument_Count
Part 3 "Usage: " & ______ Command_Name
Part 3 ______(1) Argument function
Part 3 if not ______ Exists function
Part 4 ______(F, ______, ...) Open, In_File
Part 4 while not ______(F) End_Of_File
Part 4 ______(F, Line_Buffer, Last) Get_Line
Part 5 ______(Line, ',', Token) Get_Next_Token
Part 5 ______'Value Float attribute
Part 5 Average calculation Score1, Score2, Score3
Part 5 String_Vectors.______ Append procedure
Part 5 ______(F) Close procedure
Part 6 Names.______ First_Index, Last_Index
Part 6 > ______ Highest_Avg variable
Part 6 String_Vectors.______(Names, I) Element function
Part 6 < ______ Lowest_Avg variable
Part 6 String_Vectors.______(Names) Length function
Part 7 String_Vectors.______(Names) Length function

Testing Your Program

Create a file named grades.csv with this content:

Alice,85,92,78
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92

Then test with these scenarios:

Test Command Expected Result
No arguments ./grade_analyzer Usage message displayed
Missing file ./grade_analyzer nofile.csv Error message about file not found
Valid file ./grade_analyzer grades.csv All statistics displayed correctly

Expected Values for Sample Data

Student Average
Alice 85.00
Bob 91.00
Charlie 71.67
Diana 95.00
Class Average 85.67

Common Errors to Watch For