1

In one of the textbooks I'm reading is stated "While types are checked for compatibility at compile time, subranges require run-time range checking." If I'm getting it right, a runtime check must be performed when new values are assigned to a variable that is defined in a certain subrange (subtype). Isn't it the same for types? Why this difference? Is this just a default of the compiler?

sepp2k
  • 363,768
  • 54
  • 674
  • 675
Drimades Boy
  • 437
  • 1
  • 5
  • 19
  • what language are you talking about? – Tatsuyuki Ishi Apr 01 '17 at 10:58
  • It applies to Ada and probably to other languages as well. – Drimades Boy Apr 01 '17 at 11:07
  • 1
    when you say "subrange check", do you mean a check whether specific indices fall within allowed limits when it comes to collection of elements (e.g. array)? or is it something different? – oldbam Apr 01 '17 at 11:20
  • @oldbam as I said this is an excerpt from a book and I think it's about checking values of variables of a certain subtype. – Drimades Boy Apr 01 '17 at 12:19
  • I think @oldbam is correct that the subranges refer to indices in arrays in Ada. Subtypes are in line what you are saying but that is different than subrange. – Frank C. Apr 01 '17 at 15:32

2 Answers2

8

It’s true that assigning from a subtype P of a type U won’t require checks, since there can’t be a mismatch (even if the subtype has the same range as the type; subtype P is U; is perfectly legal and can be useful!)

The other way, from the type to the subtype, will involve checks, at least if the subtype is a subrange.

From the outside, the same is true of another type with a different range. However, there’s a subtle difference; with type/subtype, you can assign one to the other without conversion, if not without checks, but with type/type you must type-convert before the assignment, and any constraint violation will be checked at the conversion, not the assignment.

This may help to illustrate the point:

procedure Drimades is
   type Upper is range 10 .. 20;
   subtype Part is Upper range 10 .. 15;
   type Lower is new Upper range 10 .. 15;

   procedure Assign_U_To_P (U : Upper; To : out Part) is
   begin
      To := U;  -- requires check
   end Assign_U_To_P;

   procedure Assign_P_To_U (P : Part; To : out Upper) is
   begin
      To := P;  -- no check needed
   end Assign_P_To_U;

   procedure Assign_U_To_L (U : Upper; To : out Lower) is
   begin
      To := Lower (U);  -- requires check
   end Assign_U_To_L;

   procedure Assign_L_To_U (L : Lower; To : out Upper) is
   begin
   To := Upper (L);  -- no check required
   end Assign_L_To_U;

   U : Upper;
   P : Part;
   L : Lower;
begin
   Assign_U_To_P (20, P);
   Assign_P_To_U (15, U);
   Assign_U_To_L (20, L);
   Assign_L_To_U (15, U);
end Drimades;

Compiling this with GNAT and the switch -gnatG produces this intermediate representation, not too hard to interpret I hope:

procedure drimades is
   type drimades__upper is range 10 .. 20;
   [type drimades__TupperB is new short_short_integer]
   freeze drimades__TupperB []
   subtype drimades__part is drimades__upper range 10 .. 15;
   [type drimades__TlowerB is new drimades__TupperB]
   freeze drimades__TlowerB []
   type drimades__lower is new drimades__upper range 10 .. 15;

   procedure drimades__assign_u_to_p (u : drimades__upper; to : out
     drimades__part) is
   begin
      [constraint_error when
        not (u in 10 .. 15)
        "range check failed"]
      to := u;
      return;
   end drimades__assign_u_to_p;

   procedure drimades__assign_p_to_u (p : drimades__part; to : out
     drimades__upper) is
   begin
      to := p;
      return;
   end drimades__assign_p_to_u;

   procedure drimades__assign_u_to_l (u : drimades__upper; to : out
     drimades__lower) is
   begin
      [constraint_error when
        not (u in 10 .. 15)
        "range check failed"]
      to := drimades__lower(u);
      return;
   end drimades__assign_u_to_l;

   procedure drimades__assign_l_to_u (l : drimades__lower; to : out
     drimades__upper) is
   begin
      to := drimades__upper(l);
      return;
   end drimades__assign_l_to_u;

   u : drimades__upper;
   p : drimades__part;
   l : drimades__lower;
begin
   drimades__assign_u_to_p (20, p);
   drimades__assign_p_to_u (15, u);
   drimades__assign_u_to_l (20, l);
   drimades__assign_l_to_u (15, u);
   return;
end drimades;
Simon Wright
  • 25,108
  • 2
  • 35
  • 62
2

Simon Wright's answer addresses the question from the point of view of Ada and its subtype, which means a subset of values in a type, thus not what Ada or C++ call a derived type or derived class, respectively. (type Lower is new Upper; makes Lower be derived from Upper, the “…” defining further characteristics of the subtype then created.)

More generally, some languages allow any variable name to refer to any type of object at run-time. Thus, in JavaScript,

var a = 15, b = -1;
  ...
a = {"foo": "bar"}
  ...
return a + b;

will return a result that might be surprising, but is fine if judged by the language. a is assigned a new value of a different type on the second line, and + will produce something from a and b on the third.

This flexibility is not wanted in languages like Ada, or C, or Swift for that matter: a program that is written in such a language, to run on any computer device, is meant to not be putting a dictionary-like object in a location declared to be of some integer type, say. Compile time type checking prevents this situation. Any “disrespect” when assigning (or passing) objects is required to be detected at compile time.

Ada, in addition to compile time type checking, uses name based equivalence of types. So,

type Apples is range 0 .. 20;
type Oranges is range 0 .. 20;

a : Apples := 5;
b : Oranges := 8;

return a + b;  -- Error! 

You get

 8.    return a + b;  -- Error!
                |
    >>> invalid operand types for operator "+"
    >>> left operand has type "Apples" defined at line 2
    >>> right operand has type "Oranges" defined at line 3

This is pure type checking at compile-time.

Last, extending what Simon Wright's example is illustrating, sometimes Ada even requires subtypes to be checked at compile time. The phrase involved then is that subtypes statically match, where static means compile-time. For example, same bounds. But, this is advanced stuff occurring, e.g., when pointers should point to objects on the stack and these have a range (subtype) that is different from the pointer's.

Community
  • 1
  • 1
B98
  • 1,229
  • 12
  • 20