usethesource / flybytes

Flybytes is an intermediate language between JVM bytecode and software languages (DSLs, PLs), for compilation and decompilation.
BSD 2-Clause "Simplified" License
16 stars 6 forks source link

Bug: Error when decompiling method with one argument #21

Closed bys1 closed 1 year ago

bys1 commented 1 year ago

I have compiled the following class with javac:

public class Test {

    public int test1() {
        return 3;
    }

    public int test2(int x) {
        return 5;
    }

}

When decompiling this using Flybytes (decompile from lang::flybytes::Decompiler), it throws an error:

|jar+file:///Users/bys1/.m2/repository/org/rascalmpl/flybytes/0.1.7/flybytes-0.1.7.jar!/src/lang/flybytes/Disassembler.rsc|(1465,237,<18,0>,<20,75>): Java("IndexOutOfBoundsException","Index 1 out of bounds for length 0")
    at jdk.internal.util.Preconditions.outOfBounds(|unknown:///Preconditions.java|(0,0,<64,0>,<64,0>))
    at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(|unknown:///Preconditions.java|(0,0,<70,0>,<70,0>))
    at jdk.internal.util.Preconditions.checkIndex(|unknown:///Preconditions.java|(0,0,<266,0>,<266,0>))
    at java.util.Objects.checkIndex(|unknown:///Objects.java|(0,0,<359,0>,<359,0>))
    at java.util.ArrayList.get(|unknown:///ArrayList.java|(0,0,<427,0>,<427,0>))
    at lang.flybytes.internal.ClassDisassembler.formals(|unknown:///ClassDisassembler.java|(0,0,<229,0>,<229,0>))
    at lang.flybytes.internal.ClassDisassembler.method(|unknown:///ClassDisassembler.java|(0,0,<205,0>,<205,0>))
    at lang.flybytes.internal.ClassDisassembler.methods(|unknown:///ClassDisassembler.java|(0,0,<178,0>,<178,0>))
    at lang.flybytes.internal.ClassDisassembler.readClass(|unknown:///ClassDisassembler.java|(0,0,<105,0>,<105,0>))
    at lang.flybytes.internal.ClassDisassembler.disassemble(|unknown:///ClassDisassembler.java|(0,0,<92,0>,<92,0>))
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(|unknown:///NativeMethodAccessorImpl.java|(0,0,<0,0>,<0,0>))
    at disassemble(|jar+file:///Users/bys1/.m2/repository/org/rascalmpl/flybytes/0.1.7/flybytes-0.1.7.jar!/src/lang/flybytes/Disassembler.rsc|(1685,5,<20,58>,<20,63>))
    at $shell$(|prompt:///|(0,48,<1,0>,<1,48>)ok
rascal>

This happens whenever the class contains a method with exactly one argument. When I remove the method test2, or remove the argument x from test2, the class decompiles fine. When I add another argument to test2, such as int y, it also decompiles without errors. However, with one argument, it throws the above error. Since I would expect Flybytes to be able to decompile such a simple Java class, I am assuming that this is a bug.

jurgenvinju commented 1 year ago

Thanks for reporting this; and especially for the simple example that triggers this bug.

jurgenvinju commented 1 year ago

I've been having some trouble reproducing the stacktrace:

rascal>decompile(|project://flybytes/target/classes/lang/flybytes/internal/Test.class|, "test2")
Method: method(
  methodDesc(
    integer(),
    "test2",
    [integer()]),
  [var(
      integer(),
      "x")],
  [return(const(
        integer(),
        5))],
  modifiers={public()})

Perhaps its because my test class is nested in a package. going to try and move it to the root now.

jurgenvinju commented 1 year ago

So we're looking at the following code, and it does depend on some contextual parameters, namely whether or not javac was called with the -debug flag, and/or the -parameters flag:

private IList formals(List<ParameterNode> parameters, List<LocalVariableNode> locals, IList types, boolean isStatic) {
        IListWriter lw = VF.listWriter();

        if (parameters != null && !parameters.isEmpty()) {
            // only when class was compiled with javac -parameters
            int i = 0;
            for (IValue elem : types) {
                lw.append(ast.Formal_var((IConstructor) elem, parameters.get(i++).name));
            }
        }
        else if (locals != null && locals.size() >= types.length() - (isStatic?0:1)) {
            // only when class was compiled with javac -debug
            int i = 0;
            for (IValue elem : types) {
                                 // HERE the exception is thrown; `locals` does not have enough elements apparantly. 
                LocalVariableNode local = locals.get(i + (isStatic?0:1));
                lw.append(ast.Formal_var((IConstructor) elem, local.name));
                i++;
            }
        }
        else {
            // otherwise we "invent" the parameter names
            int i = 0;
            for (IValue elem : types) {
                lw.append(ast.Formal_var((IConstructor) elem, "arg_" + i));
            }
        }

        return lw.done();
    }
jurgenvinju commented 1 year ago

This was after using javac Test.java without the -g and without -parameters. With either of the parameters added, the bug does not trigger.

rascal>decompile(|project://flybytes/src/Test.class|)
)
|file:///Users/jurgenv/git/flybytes/src/lang/flybytes/Disassembler.rsc|(1465,237,<18,0>,<20,75>): Java("IndexOutOfBoundsException","Index 1 out of bounds for length 0")
        at jdk.internal.util.Preconditions.outOfBounds(|unknown:///Preconditions.java|(0,0,<64,0>,<64,0>))
        at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(|unknown:///Preconditions.java|(0,0,<70,0>,<70,0>))
        at jdk.internal.util.Preconditions.checkIndex(|unknown:///Preconditions.java|(0,0,<248,0>,<248,0>))
        at java.util.Objects.checkIndex(|unknown:///Objects.java|(0,0,<372,0>,<372,0>))
        at java.util.ArrayList.get(|unknown:///ArrayList.java|(0,0,<459,0>,<459,0>))
        at lang.flybytes.internal.ClassDisassembler.formals(|unknown:///ClassDisassembler.java|(0,0,<229,0>,<229,0>))
        at lang.flybytes.internal.ClassDisassembler.method(|unknown:///ClassDisassembler.java|(0,0,<205,0>,<205,0>))
        at lang.flybytes.internal.ClassDisassembler.methods(|unknown:///ClassDisassembler.java|(0,0,<178,0>,<178,0>))
        at lang.flybytes.internal.ClassDisassembler.readClass(|unknown:///ClassDisassembler.java|(0,0,<105,0>,<105,0>))
        at lang.flybytes.internal.ClassDisassembler.disassemble(|unknown:///ClassDisassembler.java|(0,0,<92,0>,<92,0>))
        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(|unknown:///NativeMethodAccessorImpl.java|(0,0,<0,0>,<0,0>))
        at disassemble(|file:///Users/jurgenv/git/flybytes/src/lang/flybytes/Disassembler.rsc|(1685,5,<20,58>,<20,63>))
        at $shell$(|prompt:///|(0,57,<1,0>,<1,57>)ok
jurgenvinju commented 1 year ago

Ok the bug is fixed. However, it does show that the decompiler is depending on some of the debug information. You see that the constructor is not reduced to a super call anymore, but that the load instruction remains in an asm block. This is a different kind of bug though.

jurgenvinju commented 1 year ago

Need to figure out a better way to test this in the current project setup. We should have the same files compiled with different javac settings and then decompiled to comparable ASTs.