Skip to content

Instantly share code, notes, and snippets.

@Xliff
Last active July 26, 2020 12:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Xliff/600d1c8ba52ce33c6c0bcbf43fb24ccc to your computer and use it in GitHub Desktop.
Save Xliff/600d1c8ba52ce33c6c0bcbf43fb24ccc to your computer and use it in GitHub Desktop.
AnnoyinglyPersistenEnums

Is there a way to add entries without the "EDeviceActivityLevel_k' after this definition?

our enum EDeviceActivityLevelEnum is export (
  EDeviceActivityLevel_k_EDeviceActivityLevel_Unknown                 => -1,
  EDeviceActivityLevel_k_EDeviceActivityLevel_Idle                    =>  0,
  EDeviceActivityLevel_k_EDeviceActivityLevel_UserInteraction         =>  1,
  EDeviceActivityLevel_k_EDeviceActivityLevel_UserInteraction_Timeout =>  2,
  EDeviceActivityLevel_k_EDeviceActivityLevel_Standby                 =>  3,
  EDeviceActivityLevel_k_EDeviceActivityLevel_Idle_Timeout            =>  4,
)

ANSWER -- Why yes, there is! See solution in the comments.

@Xliff
Copy link
Author

Xliff commented Jul 26, 2020

Well, it wasn't easy, and it took ages to get right, but here's a way to do it. It's not actually adding to the enum. It is more like defining constant aliases that can be exported that will return the proper value.

So, say you have a compunit with just enums. You first need to have a place to hold your new exports. You need to define it at the top, our scoped and exported, otherwise it will not work:

our %OPENVR-RAW-ENUMS-NEW-EXPORTS is export;
our $export-count = 0;

Your sub EXPORT, which must be defined outside of your package definition, will just re-export what was already exported, in addition to your new definitions, and will look like this:

sub EXPORT {
  %OPENVR-RAW-ENUMS-NEW-EXPORTS.append( EXPORT::DEFAULT::.pairs )
    unless $export-count;
  $export-count++;

Now comes the tricky part. For performance reasons, we want all of the hard work to be done as late into compile-time as possible, hence a BEGIN block. This block performs the real work:

BEGIN {
  for EXPORT::DEFAULT::.pairs.grep({
    .key.defined && .key.ends-with('Enum')
  }).sort( *.key ) -> \enum  {
    my %e-pairs = enum.value.enums.pairs.Hash;
    my @e-keys = %e-pairs.keys;
    my $prefix = getLongestSubstr( |@e-keys );

    say "Enum { enum.key }: $prefix" if %*ENV<OPENVR_DEBUG>;

    my $prefix-count = count-substring($prefix, '_');
    if $prefix-count >= 1 {
      # Trim up to 3 tokens max!
      my $to = min($prefix-count, 3);
      for ^$to  {
        # cw: keep all defs from loop until entire loop is processed.
        #     if ANY collisions, drop whole set of defs!
        my $collided = False;
        my %NEW-KEYS;
        for @e-keys -> \enum-key {
          my $loop-prefix = $prefix.split(/_/)[^$_].join('_') ~ '_';
          my $nvn = enum-key.subst($loop-prefix, '');
          #say "EK: { enum-key }";
          my $nvv := ::EXPORT::DEFAULT::{enum-key};
          #say "NVV: { $nvv }";

          # Collision detection
          if [&&](
                        EXPORT::DEFAULT::{$nvn}:exists.not,
            %OPENVR-RAW-ENUMS-NEW-EXPORTS{$nvn}:exists.not
          ) {
            # cw: YYY - Bind to enum constant in EXPORT table, instead
            %NEW-KEYS{$nvn} := $nvv;
          } else {
            $collided = True;
          }
        }
        if $collided.not {
          %OPENVR-RAW-ENUMS-NEW-EXPORTS{.key} := .value for %NEW-KEYS.pairs
        } else {
          my $nk = %NEW-KEYS.keys.join(', ');
          warn  "A key in ({$nk}) already exists in EXPORT table. Skipping..."
            if %*ENV<OPENVR_DEBUG>;
        }
      }
    }
  }
}

So lets break this down. We want to walk the symbol table. For my purposes, all of my enum definitions end with Enum, so getting those out is easy:

  for EXPORT::DEFAULT::.pairs.grep({
    .key.defined && .key.ends-with('Enum')
  }).sort( *.key ) -> \enum 

We need the loop to be as deterministic as possible, so the output is sorted by key. This insures that order the enums are processed in will be the same for a given set of enums. If we just went with .pair().grep(), this would not be the case.

Now the trick is to figure out what parts to trim from the front. That work is performed by the innocuous getLongestSubstr function(). Let's take a closer look at that, since it's definition wasn't given. First we need to define another multi for the max() function:

# "Exhaustive" maximal...
multi max (:&by = {$_}, :$all!, *@list) is export {
    # Find the maximal value...
    my $max = max my @values = @list.map: &by;

    # Extract and return all values matching the maximal...
    @list[ @values.kv.map: {$^index unless $^value cmp $max} ];
}

Then, the longest substring shared between a list of strings can be found via this definition:

sub getLongestSubstr(*@strings) is export {
  (max :all, :by{.chars}, keys [∩] @strings».match(/.+/, :ex)».Str)[0]
}

(For a better idea of how that all works, note that I cribbed that algorithm from somewhere else. Conway's wrtte-up explains it all in detail.)

Now that we have the longest substring, we need to count how far down we can cut things. To prevent collisions, it's probablky wise to establish a maximum number of tokens we can cut from the enum name. I selected 3 as an arbitrary limit:

my $prefix-count = count-substring($prefix, '_');
my $to = min($prefix-count, 3);

From there, we piece together the portion of the prefix we are going to remove:

for ^$to  {
        #. ..
        my %NEW-KEYS;
        for @e-keys -> \enum-key {
          my $loop-prefix = $prefix.split(/_/)[^$_].join('_') ~ '_';
          my $nvn = enum-key.subst($loop-prefix, '');
          my $nvv := ::EXPORT::DEFAULT::{enum-key};

          if [&&](
                        EXPORT::DEFAULT::{$nvn}:exists.not,
            %OPENVR-RAW-ENUMS-NEW-EXPORTS{$nvn}:exists.not
          ) {
            %NEW-KEYS{$nvn} := $nvv;
          } else {
            $collided = True;
          }
        }
        if $collided.not {
          %OPENVR-RAW-ENUMS-NEW-EXPORTS{.key} := .value for %NEW-KEYS.pairs
        } else {
          my $nk = %NEW-KEYS.keys.join(', ');
          warn  "A key in ({$nk}) already exists in EXPORT table. Skipping..."
            if %*ENV<OPENVR_DEBUG>;
        }
      }
}

Please note, that the above adds collision detection. The basic rule here is that if we've encountered a collision at a specific token level, we are likely have more. Rather than add partial aliases we skip the whole set.

The idea is that we precompile our aliases in %OPENVR-RAW-ENUMS-NEW-EXPORTS. Then use that precompiled list at runtime when sub EXPORT() is executed.

Turns out to work fairly well.

Hope you find this gist useful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment