2

I am porting to Java a bytecode compiler written in C. In the C implementation, the set of operations is represented in an enum:

typedef enum {
  SOME_OP,
  ANOTHER_OP,
  //Etc.
} Operations;

Operations are then mixed with numeric arguments in an array defined as:

uint8_t* opsAndArgs;

For example, opsAndArgs might hold:

{SOME_OP, 42, ANOTHER_OP, 99, 03, DIFFERENT_OP, RANDOM_OP, 14, etc.}

Finally, a function in the C program iterates through opsAndArgs and switches on operators, consuming arguments as appropriate:

int i = 0;

while (i < opsAndArgs.length) {
  switch (opsAndArgs[i] {
    case SOME_OP:
      handleSomeOp(opsAndArgs[i+1]);
      i = i + 2; break;
    case ANOTHER_OP:
      handleAnotherOp(opsAndArgs[i+1], opsAndArgs[i+2]);
      i = i + 3; break;
    case DIFFERENT_OP:
      handleDifferentOp();
      i = i + 1; break;
    Etc.
  }
}

Is there a way I can do the same thing in Java? I.e., create an Enum that I can mix into an array with numeric values, and still have the ability to switch on Enum-defined operators while treating the remaining values as arguments?

It seems like the original program is taking advantage of some feature of C enums such that they are mix-able with uint8_t values.

2
  • 3
    In C an enum is essentially a set of named integer constant(s). So you could use integer constants in Java to get the same effect (and very similar code). Or you could create an enum that can be converted to and from integers Commented Jun 4, 2022 at 16:31
  • 1
    you can use a hashtable to map the string of the opcode to the integer value of it, this has the benefit of being able to make a similar hashtable to reverse the opcode back into the string representation. eg HashMap<String, Integer> opsAndArgs= new HashMap<String, Integer>() {{ put("SOME_OP", 0); put("ANOTHER_OP", 1); put("DIFFERENT_OP", 2); put("RANDOM_OP", 3);}};. Then you can opsAndArgs["RANDOM_OP"] to get your value back. Alternatively you can hardcode the each constant similar to final static int RANDOM_OP = 3; in the class you are using them, or another Operations class. Commented Jun 4, 2022 at 17:51

3 Answers 3

2

Unlike in C, an enum in Java is an object. That means that the only way to mix the them with numbers is with a List<Object> or Object[].

Every enum does have an ordinal() value that is an int. These are not compile-time constants though, so you can't use them in switch statements*.

I think that you have some options:

  1. Go for the List<Object> / Object[] solution. If you only perform switching on the operations, you can do this with a combination of instanceof and switch
if (opsAndArgs[i] instanceof Operation operation) { // **
    switch (operation) {
        case SOME_OP: ...
    }
}
  1. Forget the enum and go for a constants class:
final class Operation {
    public static final int SOME_OP = 1;
    // etc

    private Operation() {}
}
  1. Define int constants for each enum constant. Each enum constant must return one of these, and you'll need to have a lookup (static method) on the enum class to find the enum constant based on the int constant. Don't trust on ordinal() as constants because if you change the order of enum constants the ordinal values change as well.

I think the main question you have to ask yourself is: do you need an enum? Or is the constants class good enough? Because that's definitely the easier of the solutions.

* Yet? I'm not fully up-to-speed with the latest changes to switch pattern matching...

** That's syntax finalized in Java 16. In older versions, use opsAndArgs[i] instanceof Operation in combination with a new variable inside the if: Operation operation = (Operation) opsAndArgs[i];

Sign up to request clarification or add additional context in comments.

Comments

2

From my point of view:

  • Keep the structure of the C byte code interpreter, else life gets too complicated!

  • opsAndArgs has to be a byte array. We are talking about Java byte code here. Some values have to be interpreted as bytes, some as shorts, ... and the program counter needs to advance in byte-sized steps.

  • Use static final byte for opcodes.

Comments

1

Here's a working solution in Java. Obviously, there are some things that were unclear (for example, I don't know what your handleXXXXOp methods do), so liberties were taken. Otherwise, this works just fine in Java 17.

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

public class SOQ_20220605_2
{

   private sealed interface Value
   {
   
      public static List<Value> asList(Object... uncheckedValues)
      {
      
         Objects.requireNonNull(uncheckedValues);
         
         final List<Value> values = new ArrayList<>();
         
         for (var each : uncheckedValues)
         {
         
            values.add(
               switch(each)
               {
               
                  case null         -> throw new NullPointerException("No nulls allowed");
                  case Integer i    -> new Argument(i);
                  case String s     -> Operation.valueOf(s.toUpperCase());
                  case Argument a   -> a;
                  case Operation o  -> o;
                  default           -> throw new IllegalArgumentException("Unexpected type! each = " + each);
               
               }
            );
         
         }
         
         return values;
      
      }
   
   }
   
   private enum Operation implements Value
   {
      
      SOME_OP,
      ANOTHER_OP,
      DIFFERENT_OP,
      RANDOM_OP,
      ;
      
   }
   
   private record Argument(int num) implements Value {}

   public static void main(String[] args)
   {
   
      final List<Value> opsAndArgs =
         Value.asList
         (
            "SOME_OP",
            42,
            "ANOTHER_OP",
            99,
            3,
            "DIFFERENT_OP",
            "RANDOM_OP",
            14
         );

         final int result = calculateResult(opsAndArgs);
         
         System.out.println(result);

   }
   
   private static int calculateResult(List<Value> opsAndArgs)
   {
   
         int i = 0;

         while (i < opsAndArgs.size())
         {
         
            if (opsAndArgs.get(i) instanceof Operation operation)
            {
            
               i = 
                  switch (operation)
                  {
               
                     case SOME_OP -> 
                     {
                     
                        handleSomeOp(
                              opsAndArgs.get(i + 1)
                           );
      
                        yield i + 2;
                     
                     }
                     
                     case ANOTHER_OP ->
                     {
                     
                        handleAnotherOp(
                              opsAndArgs.get(i + 1),
                              opsAndArgs.get(i + 2)
                           );
      
                        yield i + 3;
                     
                     }
                  
                     case DIFFERENT_OP ->
                     {
                     
                        handleDifferentOp();
      
                        yield i + 1;
                     
                     }
                     
                     case RANDOM_OP ->
                     {
                        
                        yield i + 4;
                        //throw new UnsupportedOperationException("This has not been implemented yet.");
                     
                     }
                  
                  };
               
            }
         
         }
         
         return i;
   
   }
   
   private static void handleSomeOp(Value value)
   {
   
      //do something
   
   }

   private static void handleAnotherOp(Value value1, Value value2)
   {
   
      //do something
   
   }

   private static void handleDifferentOp()
   {
   
      //do something
   
   }

}

We create a common interface called Value, so that we can have a domain of acceptable values for our List<Value>. Then, we make it sealed, so that only the types we allow can be considered a Value. Those 2 types are Argument and Operation. We also have a method asList that handles taking in the array that you described in your question. Now, you can pass in your operations and arguments as parameters. If you wanted to pass in SOME_OP, you can either pass it in as Operation.SOME_OP or as a String via "SOME_OP". The method can handle both ways currently, but I am choosing to pass in each operation as a String. Same for the arguments. If you wanted to pass in 42, you could either pass it in as new Argument(42), or you could pass it in naked as an int via 42. The method can handle both ways currently, but I am choosing to pass in each argument as an int.

Operation is an enum that holds all of the operations you defined. If you want to add more operations, simply add them to this enum, compile, then update any of the switch statements that now fail compilation.

Argument is a record that holds arguments. Having it be an explicit type helps us separate the arguments from the operations when they are together.

The main method gathers up the values in basically the same way you did, then calls the method calculateResult, with our List<Value> values as the parameter.

calculateResult is basically the switch statement and while loop that you had in your original question. This is where most of the liberties were taken. And your switch case didn't specify what happens when it comes across an argument, so I built my solution to silently ignore arguments in the switch statement.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.