Exhaustive Enumeration Case

Is this Enterprise only?

The stackoverflow answer was from 2016.

delphi - Function result as enumerated type - how to proof all of variants are in case section - Stack Overflow https://share.google/4Z9cSRGqZRRYPCRsO

(In fact, saying ‘Switch’ .. maybe it refers to C++ ?)

QA audits were removed in Florence. Prior to that the enum case audit was only available in Enterprise (Professional only included some basic metrics).

I routinely use this pattern for case statements:

  case EnumVar of
    TEnumType.One: ...;
    TEnumType.Two: ...;
  else
    raise Exception.Create('Unhandled enum type = ' + Spring.TEnum.GetName<TEnumType>(EnumVar));
  end;

Obviously that is not possible in a function that has more complex conditions. You may be able to use AI to perform an audit, otherwise add sufficient unit tests and make sure that they are kept up to date with any changes to the enum type :slight_smile:

One AI answer mentioned tying the cases into interface methods .. but I’m working and haven’t had time to think about it or digest that idea.

I do the same. It’s not perfect but it’s saved me a few times over the years.

Reply mostly from Claude Opus 5.5 :

I assume the main purpose of the above is to catch an extended enum without an extended case.

Claude mentioned the possibility of looking past the defined values …

What does it do at the edges? Tested in (Delphi 13, Win32 and Win64, Spring4D 2.0.1).

  • When the enum gains a value and the case isn’t updated, it works as intended:
    Unhandled enum type = MyNewEnumVal. :white_check_mark:

  • When the value itself is bad, e.g. a cast from an integer column or a byte read from disk (TMyEnum(5) with $R-), the else is reached too, and the name lookup becomes the weak spot:

    • System.TypInfo.GetEnumName doesn’t check the range. One past the end returns the program’s name. Further out, it returns whatever bytes are nearby; in my test, a fragment of a string literal from elsewhere in the program. No exception.
    • Spring’s TEnum.GetName does check, and raises EInvalidEnumArgumentException. But the message comes out as Invalid enum argument: value, because SInvalidEnumArgument has one %s and Guard.CheckEnum passes three arguments (name, type name, value). The type and the bad value are lost, and our own message is never built.
      Probably a one-line fix in Spring: something like ‘Invalid enum argument %s: %s = %d’.
      Maybe @sglienke could verify whether this is true or false?

Proposed :

▎ else
▎   raise EInvalidOpException.CreateFmt('Unhandled %s: ordinal %d', ['TMyEnum', Ord(EnumVar)]);

Two related things :

  • In a function, a case covering every enum value with no else still gets W1035 (return value might be undefined). The compiler can’t tell a complete case from an incomplete one, so it warns either way.
    An else raise (or a call to a noreturn procedure) makes it go away.
  • In a procedure, a case with no else that misses a new value gets no warning at all. It just does nothing.

I’m working on creating / extending new syntax in a preprocessor for standard Delphi.

Matching would be exhaustive, so something like …

match N of
  | 1, 3, 5, 7, 9 : 'odd'
  | 0, 2, 4, 6, 8 : 'even'
  | _             : 'out of range'  // Leaving out | _: on an Integer is a compile error.
end

In the output, the _ arm becomes the case’s ordinary else.

case N of
  1, 3, 5, 7, 9: Result := 'odd';
  0, 2, 4, 6, 8: Result := 'even';
else
  Result := 'out of range';
end;

and for enums (the first proposal) …

type
  TTxn =
    | Cheque(Number: Integer; Drawee: TBankId)
    | Wire  ( Swift: TSwiftCode)
    | Cash
    | Card  ( Last4: TDigits4; Auth: TAuthCode);

function Describe(const T: TTxn): string;
begin
  Exit( match T of
        | Cheque(No, Bank) : Format('cheque %d on %s', [No, Bank])
        | Wire(Code)       : 'wire via ' + Code
        | Cash             : 'cash'
        | Card(Last4, _)   : 'card ending ' + Last4
        end 
      );
end;

translated to …

procedure __dpp_Unreachable; noreturn;
begin
  raise EInvalidOpException.Create('unreachable');
end;

function Describe(const T: TTxn): string;
begin
  case T.Kind of
    tkCheque: Exit( Format('cheque %d on %s', [T.Number, T.Drawee]) );
    tkWire:   Exit( 'wire via ' + T.Swift );
    tkCash:   Exit( 'cash' );
    tkCard:   Exit( 'card ending ' + T.Last4 );
  else
    __dpp_Unreachable;
  end;

Why do you ask me if you have AI that can write you a little program? :wink:

uses
  Spring,
  System.SysUtils;

type
  TEnumType = (One,Two,Three);

procedure Bonk(EnumVar: TEnumType);
begin
  case EnumVar of
    TEnumType.One: ;
    TEnumType.Two: ;
  else
    raise Exception.Create('Unhandled enum type = ' + Spring.TEnum.GetName<TEnumType>(EnumVar));
  end;
end;

begin
  try
    Bonk(Three);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  try
    Bonk(TEnumType(4));
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  Readln;
end.

Yes because as you can see the compiler does not prevent any out of range values for enum (and other ordinal types) on hardcasts. Thus although the case might be exhaustive on valid values, the variable can still contain invalid values.

Did someone say Delphi is a safe language? :rofl:

  • Well for one, I trust you will be correct.
  • Two, I was trying not to side-track too much.
  • Three, my 12.3 & 13.1 are both giving me
    “[dcc32 Fatal Error] Project2.dpr(6): F2051 Unit Spring was compiled with a different version of System.Generics.Collections.TList`1.InsertRange”
    atm, for all the versions in DPM. I dunno why. I cleaned, I deleted dcus.

But … Claude is able to run it locally, in some scratch directory. :eyes:

Reported output :

Exception: Unhandled enum type = Three
EInvalidEnumArgumentException: Invalid enum argument: value

It occurs to me that our code/compilers sometimes can’t be smart enough to tell when a case is exhaustive …

case x:integer of
   Odd(x)  : ... ;
   Even(x) : ... ;
end;