paulirwin / JavaToCSharp

Java to C# converter
MIT License
267 stars 90 forks source link

Translate enums with values #19

Open paulirwin opened 3 years ago

paulirwin commented 3 years ago

I think we can translate enums with values to be sealed classes with public static readonly members.

Example:

public enum LineSeparator {
    CR("\r", "CR (\\r)"),
    LF("\n", "LF (\\n)"),
    CRLF("\r\n", "CRLF (\\r\\n)");

    private final String text;
    private final String description;

    LineSeparator(String text, String description) {
        this.text = text;
        this.description = description;
    }

    // ... other methods here
}

Output:

public sealed class LineSeparator 
{
    public static readonly LineSeparator CR = new LineSeparator("\r", "CR (\\r)");

    public static readonly LineSeparator LF = new LineSeparator("\n", "LF (\\n)");

    public static readonly LineSeparator CRLF = new LineSeparator("\r\n", "CRLF (\\r\\n)");

    private readonly string text;
    private readonly string description;

    private LineSeparator(string text, string description)
    {
        this.text = text;
        this.description = description;
    }

    // ... other methods here
}
uxmal commented 3 years ago

How are you going to generate switch statements using this construct?

paulirwin commented 3 years ago

@uxmal Short answer - with this approach, we wouldn't.

There are plenty of cases today where it is just infeasible to translate all the way down, and we tend to stick to a syntactic translation only. This results in compiler errors in the resulting code quite often that need to be manually fixed, and that's okay. In this case, I personally think it's more important to preserve the values of the "enum" and its behavior than to eschew them in favor of making switch work, since switch can be replaced with if in this case.

However, your question got me thinking about an alternative approach. We could translate enums with values to be an abstract class with concrete implementations for each value. Example:

public abstract class EnumTest
{
    public static readonly EnumTest A = new EnumTest_A();
    public static readonly EnumTest B = new EnumTest_B();

    protected EnumTest(int value)
    {
        this.Value = value;
    }

    public int Value { get; }

    public class EnumTest_A : EnumTest
    {
        public EnumTest_A()
            : base(1)
        {
        }
    }

    public class EnumTest_B : EnumTest
    {
        public EnumTest_B()
            : base(2)
        {
        }
    }
}

Then, switch could be altered to use pattern matching on the type:

var value = DateTime.Now.Second % 2 == 0 ? EnumTest.A : EnumTest.B;

switch (value)
{
    case EnumTest.EnumTest_A:
        Console.WriteLine("A");
        break;
    case EnumTest.EnumTest_B:
        Console.WriteLine("B");
        break;
}

Note that with the sealed class approach, you could do pattern matching on the value, if that works for your use case:

var value = DateTime.Now.Second % 2 == 0 ? EnumTest.A : EnumTest.B;

switch (value)
{
    case { Value: 1}:
        Console.WriteLine("A");
        break;
    case { Value: 2}:
        Console.WriteLine("B");
        break;
}

We don't do any kind of symbol resolution to know that this would need pattern matching, so either approach would have to be done manually by the user... but it's certainly an option.

I'm thinking we can have three configurable approaches for how to treat enums with values:

  1. Discard values (current state) - possibly could add methods as extension methods on the enum type, but it still would lose values, which likely defeats the point. However some people might want to do it this way to keep using switch.
  2. Sealed class with public static readonly values - loses ability to do switch without pattern matching on a property value, but (IMHO) cleaner code
  3. Abstract class with implementation for each enumeration value - can switch with pattern matching on the type

And then it's up to the user of JavaToCSharp to decide how they want to translate it, until C# adds proper discriminated unions. If anyone has any other ideas without involving symbol resolution, let me know!

paulirwin commented 3 years ago

Also my last example clearly could be rewritten as a standard enum with an int value... so pretend there's other fields there 😄

paulirwin commented 3 years ago

Fourth option, for those running C#9... we could take the approach from the link I posted about discriminated unions, and make them records inside an abstract class:

abstract partial class Shape
{
    public record Rectangle(float Width, float Length) : Shape;
    public record Circle(float Radius) : Shape;
}
paulirwin commented 3 years ago

... actually I'm not sure if that has the same semantics. Still thinking about this...

whizzter commented 3 years ago

Hi, just found this project (got tons of old Java utils for small projects and was looking to convert one that happened to use this).

Since all Enum's extend java.lang.Enum that has an ordinal() final method, shouldn't that be considered the route to handling the switches if one wants to re-do translations often (or are the conversions more looking at idiomatic C# rather than good/easy conversion?), a shim of that one could of course.

As for abstract classes you kinda would need support translating like that anyhow, the code i was looking to translate looks like this with enums implementing abstract methods.

    enum BinaryOperator {
        PLUS("+",false) {
            Object apply(Object l,Object r) {
                if (l instanceof Double || r instanceof Double) {
                    return ((Number)l).doubleValue()+((Number)r).doubleValue();
                } else if (l instanceof Long || r instanceof Long) {
                    return ((Number)l).longValue()+((Number)r).longValue();
                } else if (l instanceof Integer || r instanceof Integer) {
                    return ((Number)l).intValue()+((Number)r).intValue();
                }
                throw new RuntimeException("+ not defined for "+l+" and "+r);
            }
        },
        MINUS("-",false) {
            Object apply(Object l,Object r) {
                if (l instanceof Double || r instanceof Double) {
                    return ((Number)l).doubleValue()-((Number)r).doubleValue();
                } else if (l instanceof Long || r instanceof Long) {
                    return ((Number)l).longValue()-((Number)r).longValue();
                } else if (l instanceof Integer || r instanceof Integer) {
                    return ((Number)l).intValue()-((Number)r).intValue();
                }
                throw new RuntimeException("- not defined for "+l+" and "+r+" "+l.getClass());
            }
        }
        ;
        String op;
        boolean allowNull;
        BinaryOperator(String op,boolean an) {
            this.op=op;
            this.allowNull=an;
        }
        abstract Object apply(Object l,Object r);
    }
paulirwin commented 3 years ago

@whizzter Thanks for the real-world example. I had never (or rarely) encountered abstract methods in enums but you're right, that strengthens the case for the abstract class approach.

whizzter commented 3 years ago

Well to be honest I'm not sure how much this is used in other places, It's was kinda specific but quite smooth solution to binding stuff for this interpreter.

I guess this is the kind of places where you need to consider if Java compatibility or C# target code elegantness/idiomaticness is more important. (Nr 1 is more important if you want to continually translate some cross-language project , Nr 2 if you want to migrate).

Might even be that having 2 variants depending on the source can be the right choice? Abstract being enforced for compatible conversions where required, but a leaner method chosen for non-tricky situations or explicitly for a simpler idiomatic migration-mode.

maximilien-noal commented 4 months ago

This enum was converted to a C# enum with bit shifting and extension methods:

public enum Button : byte
{
    RIGHT = (0x01 << 4) | 0x10,
    B = (0x02 << 4) | 0x20,
    LEFT = (0x02 << 4) | 0x10,
    A = (0x01 << 4) | 0x20,
    UP = (0x04 << 4) | 0x10,
    SELECT = (0x04 << 4) | 0x20,
    DOWN = (0x08 << 4) | 0x10,
    START = (0x08 << 4) | 0x20
}

public static class ButtonExtensions
{
    public static byte GetMask(this Button button)
    {
        return (byte)((byte)button >> 4);
    }

    public static byte GetLine(this Button button)
    {
        return (byte)((byte)button & 0x0F);
    }
}

The original Java code:

    enum Button {
        RIGHT(0x01, 0x10), LEFT(0x02, 0x10), UP(0x04, 0x10), DOWN(0x08, 0x10),
        A(0x01, 0x20), B(0x02, 0x20), SELECT(0x04, 0x20), START(0x08, 0x20);

        private final int mask;

        private final int line;

        Button(int mask, int line) {
            this.mask = mask;
            this.line = line;
        }

        public int getMask() {
            return mask;
        }

        public int getLine() {
            return line;
        }
    }

For enums with more complicated private fields, I had to convert them into abstract classes:

public abstract class AbstractArgument
{
    protected string label;
    protected int operandLength;
    protected bool memory;
    protected DataType dataType;

    protected AbstractArgument(string label, int operandLength, bool memory, DataType dataType)
    {
        this.label = label;
        this.operandLength = operandLength;
        this.memory = memory;
        this.dataType = dataType;
    }

    public abstract int Read(Registers registers, IAddressSpace addressSpace, int[] args);
    public abstract void Write(Registers registers, IAddressSpace addressSpace, int[] args, int value);

    public int OperandLength => operandLength;

    public bool IsMemory => memory;

    public DataType DataType => dataType;

    public string Label => label;

    public static AbstractArgument Parse(string @string)
    {
        AbstractArgument[] arguments = [new ArgumentA(), new ArgumentB() /*... add other arguments here ...*/];

        foreach (AbstractArgument a in arguments)
        {
            if (a.Label.Equals(@string))
            {
                return a;
            }
        }

        throw new ArgumentException("Unknown argument: " + @string);
    }
}

public sealed class ArgumentA : AbstractArgument
{
    public ArgumentA() : base("A", 0, false, DataType.D8) { }

    public override int Read(Registers registers, IAddressSpace addressSpace, int[] args)
    {
        return registers.A;
    }

    public override void Write(Registers registers, IAddressSpace addressSpace, int[] args, int value)
    {
        registers.A = value;
    }
}

public sealed class ArgumentB : AbstractArgument
{
    public ArgumentB() : base("B", 0, false, DataType.D8) { }

    public override int Read(Registers registers, IAddressSpace addressSpace, int[] args)
    {
        return registers.B;
    }

    public override void Write(Registers registers, IAddressSpace addressSpace, int[] args, int value)
    {
        registers.B = value;
    }
}

public sealed class ArgumentC : AbstractArgument
{
    public ArgumentC() : base("C", 0, false, DataType.D8) { }

    public override int Read(Registers registers, IAddressSpace addressSpace, int[] args)
    {
        return registers.C;
    }

    public override void Write(Registers registers, IAddressSpace addressSpace, int[] args, int value)
    {
        registers.C = value;
    }
}

The original Java code:

package eu.rekawek.coffeegb.cpu.op;

import eu.rekawek.coffeegb.AddressSpace;
import eu.rekawek.coffeegb.cpu.BitUtils;
import eu.rekawek.coffeegb.cpu.Registers;

import static eu.rekawek.coffeegb.cpu.BitUtils.toSigned;

public enum Argument {

    A {
        @Override
        public int read(Registers registers, AddressSpace addressSpace, int[] args) {
            return registers.getA();
        }

        @Override
        public void write(Registers registers, AddressSpace addressSpace, int[] args, int value) {
            registers.setA(value);
        }
    }, B {
        @Override
        public int read(Registers registers, AddressSpace addressSpace, int[] args) {
            return registers.getB();
        }

        @Override
        public void write(Registers registers, AddressSpace addressSpace, int[] args, int value) {
            registers.setB(value);
        }
    }
}

Just my 2 cents.