"subtypes of the subprogram for the derived type are taken from the parent type"

Hi,
I found this rule about non-tagged derived types (or maybe that’s only for numeric types ?) which I don’t get at all:

with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Main is
  package P is
    type T1 is range 1..100;
    procedure Proc(X: T1; Y: T1);
    type T2 is new T1 range 1..50;
  end P;
  package body P is
    procedure Proc(X: T1; Y: T1) is
    begin
      Put(Integer(X)); 
      Put(Integer(Y));
    end Proc;
  end P;
use P; 
  Z: T2 := 10;
begin
  Proc(Z, 99);
end Main;

I would have expected this to fail, because Z is T1, not T2, and it’s a different type altogether. There should be no implicit conversion. The book’s explanation is “Prints 10 99. The subtypes of the subprogram for the derived type are taken from the parent type.” which makes no sense to me. Moreover, Proc (99,99) fails because of an ambiguity, but there are none, 99 can only be a T1 value.

Another rule: Can you confirm that the following is a bug ? The same function in expression forms does not call the universal_fixed operation, despite “Standard.” but the normal function does.

with Ada.Text_IO;
procedure Main is
  type Money is delta 0.01 digits 12;
  type Rates is delta 0.00000001 digits 11;
  Conversion: constant array(1..2, 1..2) of Rates := ((1.0, 0.5059), (1.9767, 1.0));
  Special: constant Rates := 1.1;
  Extra: Boolean := True;
  function "*"(Left, Right: Rates) return Rates is
  begin
    if Extra then
      return Standard."*"(Left, Right+0.01);
    else 
      return Standard."*"(Left, Right);
    end if;
  end "*";
   function "*"(Left, Right: Rates) return Rates is (Standard."*"(Left, Right+(if Extra then 0.01 else 0.0))); -- FAILS
begin
 null;
end Main;

Proc is a “primitive subprogram” of T1 and will be inherited by T2. Since the parameters used are not “class-wide” (referring not to OOP classes, but to the “class” of all possibly derived types), the procedure isn’t dynamically dispatched (virtual function in C++ or Java parlance).

From the perspective of the programmer, there’s two “Proc” procedures, Proc(T1, T1) and Proc(T2,T2), but to the backend, they’re really the same procedure. You get this subprogram “for free” along with the protections of the differences in constraint checks done at call time, but there’s no additional version of it, which you can confirm by looking at the executable. I vaguely remember Barnes’ book saying something about how this is part of what makes creating new types for semantic purposes cheap at runtime.

 $ objdump --syms bin/sample  | grep proc
00000000004036f6 l     F .text	000000000000005a              sample__p__proc.0

Proc (99,99) fails because of an ambiguity

99 isn’t a T1, it’s an integer literal, so it’s considered a universal_integer which can be treated like any integer type.

T1 and T2 are actually names for the first subtype of their respective types, and constraints are part of subtypes, not of types. Types are only characterized by a set of values and primitive operations, the actual types underneath the T1 and T2 subtypes are simply discrete signed integer types with the Proc subprogram (and implicitly created ones like for arithmetic operators) and the compiler can’t select one version of the procedure over another.

The fascinating thing is that if you shortened the longer version to use the same expression as the expression function, it works, so it seems to be something about expression functions in particular.

function "*"(Left, Right: Rates) return Rates is
begin
  return (Standard."*"(Left, Right+(if Extra then 0.01 else 0.0)));
end "*";

Playing around in Godboltwhich supports Ada!, this works:

  function "*"(Left, Right: Rates) return Rates;
  function "*"(Left, Right: Rates) return Rates is (Standard."*"(Left, Right  +(if Extra then 0.01 else 0.0)));

This reads like a bug since it seems like a violation of ARM 8.3(18/3), but it’s late and I’m sure someone can explain why I’m wrong.

For a package_declaration, generic_package_declaration, subprogram_body, or expression_function_declaration, the declaration is hidden from all visibility only until the reserved word is of the declaration;

Proc is inherited. Maybe this model will help:

type T1_Copy is new T1;            -- Clone type T1
subtype T2 is T1_Copy range 1..50; -- Create a subtype of the clone
1 Like

I can confirm, expression form is a bug! :sunglasses: You cannot throw it out of the language, but you can stop using it in your code.

It is the same body inside, I know it. But Proc is not defined for universal_integer, or T2’Base, but T1 or T2. From the programmer’s persAs defined both parameters of Proc should pass the same range constraints, both should be either of T1, or T2. Z is of T2 so in that call so the other parameter should be checked for the same constraints Z is, whether I entered a variable or a literal. It is what that procedure profile means. Literals should be treated as values of a given subtype, when it is not ambiguous. I see this as a breach of strong typing imho.

It actually is. It a model of [sub]type derivation by constraining. You need a different model or manual type creation.

Same here, literals undergo [cloning+] constraining rather than removal.

Another derivation method is represented by tagged types. But in order to have that for all types you need a major Ada type system overhaul:

  • Allowing external tags, you do not want the tag in T1, only in T1’Class.
  • Allowing copying semantics when tags are external. Presently tagged types are by reference and view conversions like T1 (X) when X is T2 are void. With external tags you might have to physically convert values.
  • Multimethods. Multimethod is a case of multiple dispatch when arguments and results belong to the same class. E.g. operations like “+” are multimethods: two arguments + result. The dispatching table is a 3D array.

Derivation by constraining is far simpler, which is why Ada 83 adopted it. Ada 95 added a very limited tagged model and already that caused a whole uprising by people talking nonsense why OO is bad, till the present day…

The key notion is “corresponding subtype” as defined in Ada RM 3.4(18/3-21). The inherited subprogram makes use of the corresponding subtype for each appearance of a subtype of the parent type in the profile of the primitive subprogram. The actual subtype constraint specified when declaring the derived type is not used in the inherited subprograms.

2 Likes

The book’s explanation is wrong. Also there is an ambiguity, the reason is exactly the reason that the book’s explanation is wrong. — I will now explain:

  1. You are declaring a type, T1.
  2. You are making a primitive operation of T1, called Proc.
  3. Toy are deriving a new type, T2 from T1.
  4. This derivation “copies” the Proc of T1, but for T2.
  5. Because Proc(99,99) takes two literals, there is nothing to disambiguate which Proc should be used: the one for T1, or the one for T2.