Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Compiler] VerifyError for record whose canonical constructor unconditionally throws exception #487

Closed
Marcono1234 opened this issue Oct 22, 2022 · 3 comments · Fixed by #840
Assignees
Labels
bug Something isn't working compiler Eclipse Java Compiler (ecj) related issues
Milestone

Comments

@Marcono1234
Copy link

Version

Eclipse Compiler 3.31.0
(Maven: org.eclipse.jdt:ecj:3.31.0)

Description

The Eclipse compiler seems to produce invalid byte code for a record class with at least one component whose canonical constructor unconditionally throws an exception:

Error: Unable to initialize main class RecordTest
Caused by: java.lang.VerifyError: Control flow falls through code end
Exception Details:
  Location:
    RecordTest.<init>(Ljava/lang/String;)V @17: <invalid>
  Reason:
    Error exists in the bytecode
  Bytecode:
    0000000: 2ab7 000a bb00 0d59 b700 0fbf 2a2b b500
    0000010: 10
  Stackmap Table:
    full_frame(@12,{Object[#1],Object[#20]},{})

I am not very familiar with Java byte code, but I assume the issue is that the compiler generates unreachable default code which tries to assign the record component fields. javap output:

 0: aload_0
 1: invokespecial #10                 // Method java/lang/Record."<init>":()V
 4: new           #13                 // class java/lang/RuntimeException
 7: dup
 8: invokespecial #15                 // Method java/lang/RuntimeException."<init>":()V
11: athrow
12: aload_0
13: aload_1
14: putfield      #16                 // Field s:Ljava/lang/String;

This is a quite contrived case, but it should probably nonetheless produce valid Java byte code.
This issue also occurs when the throw statement is wrapped with if (true) { ... }.

Reproduction steps

  1. Create a file called RecordTest.java with the following content
    record RecordTest(String s) {
        RecordTest {
            throw new RuntimeException();
        }
        
        public static void main(String... args) throws Exception {
            new RecordTest("");
        }
    }
  2. Compile the class with the Eclipse compiler:
    java -jar ./ecj-3.31.0.jar -17 ./RecordTest.java
    
  3. Try to run the compiled class
    java RecordTest
    ❌ Bug: It fails with a VerifyError
@trancexpress
Copy link
Contributor

javap -verbose output for the .class compiled by javac

  RecordTest(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: new           #7                  // class java/lang/RuntimeException
         7: dup
         8: invokespecial #9                  // Method java/lang/RuntimeException."<init>":()V
        11: athrow

And for the one from ecj:

  RecordTest(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Record."<init>":()V
         4: new           #13                 // class java/lang/RuntimeException
         7: dup
         8: invokespecial #15                 // Method java/lang/RuntimeException."<init>":()V
        11: athrow
        12: aload_0
        13: aload_1
        14: putfield      #16                 // Field s:Ljava/lang/String;

Stack trace that generates the putfield after the athrow:

"Compiler Processing Task" #199 daemon prio=5 os_prio=0 cpu=1.52ms elapsed=35.86s tid=0x00007fbd9c2429e0 nid=0x611e at breakpoint [0x00007fbe66bfe000]
   java.lang.Thread.State: RUNNABLE
        at org.eclipse.jdt.internal.compiler.ast.Reference.fieldStore(Reference.java:124)
        at org.eclipse.jdt.internal.compiler.ast.FieldReference.generateAssignment(FieldReference.java:227)
        at org.eclipse.jdt.internal.compiler.ast.Assignment.generateCode(Assignment.java:139)
        at org.eclipse.jdt.internal.compiler.ast.Expression.generateCode(Expression.java:767)
        at org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration.internalGenerateCode(ConstructorDeclaration.java:458)
        at org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration.generateCode(ConstructorDeclaration.java:310)
        at org.eclipse.jdt.internal.compiler.ast.TypeDeclaration.generateCode(TypeDeclaration.java:761)
        at org.eclipse.jdt.internal.compiler.ast.TypeDeclaration.generateCode(TypeDeclaration.java:831)
        at org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration.generateCode(CompilationUnitDeclaration.java:413)
        at org.eclipse.jdt.internal.compiler.Compiler.process(Compiler.java:916)
        at org.eclipse.jdt.internal.compiler.ProcessTaskManager.run(ProcessTaskManager.java:145)
        at java.lang.Thread.run([email protected]/Thread.java:833)

On the stack trace, I see codeStream.methodDeclaration (of type CompactConstructorDeclaration) with contents (at Expression.generateCode()):

RecordTest(String s) {
  super();
  throw new RuntimeException();
  this.s = s;
}

this.s = s is appended with this stack trace:

"Compiler Processing Task" #239 daemon prio=5 os_prio=0 cpu=9.28ms elapsed=58.51s tid=0x00007fbdb412fba0 nid=0x6841 runnable  [0x00007fbe943da000]
   java.lang.Thread.State: RUNNABLE
        at org.eclipse.jdt.internal.compiler.ast.CompactConstructorDeclaration.checkAndGenerateFieldAssignment(CompactConstructorDeclaration.java:93)
        at org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration.analyseCode(ConstructorDeclaration.java:207)
        at org.eclipse.jdt.internal.compiler.ast.CompactConstructorDeclaration.analyseCode(CompactConstructorDeclaration.java:43)
        at org.eclipse.jdt.internal.compiler.ast.TypeDeclaration.internalAnalyseCode(TypeDeclaration.java:951)
        at org.eclipse.jdt.internal.compiler.ast.TypeDeclaration.analyseCode(TypeDeclaration.java:304)
        at org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration.analyseCode(CompilationUnitDeclaration.java:137)
        at org.eclipse.jdt.internal.compiler.Compiler.process(Compiler.java:911)
        at org.eclipse.jdt.internal.compiler.ProcessTaskManager.run(ProcessTaskManager.java:145)
        at java.lang.Thread.run([email protected]/Thread.java:833)

Possibly the fix is as simple as changing the addition order:

diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CompactConstructorDeclaration.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CompactConstructorDeclaration.java
index 391b860990..e9c4eb35b2 100644
--- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CompactConstructorDeclaration.java
+++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CompactConstructorDeclaration.java
@@ -87,8 +87,8 @@ public class CompactConstructorDeclaration extends ConstructorDeclaration {
                int len = this.statements.length;
                int fLen = fa.length;
                Statement[] stmts = new Statement[len + fLen];
-               System.arraycopy(this.statements, 0, stmts, 0, len);
-               System.arraycopy(fa, 0, stmts, len, fLen);
+               System.arraycopy(fa, 0, stmts, 0, fLen);
+               System.arraycopy(this.statements, 0, stmts, fLen, len);
                this.statements = stmts;
        }
 }
\ No newline at end of file

trancexpress added a commit to trancexpress/eclipse.jdt.core that referenced this issue Mar 15, 2023
The following code results in generating instructions after an athrow
byte code statement:

record X(String s) {
    X {
        throw new RuntimeException();
    }
}

This is due to
CompactConstructorDeclaration.checkAndGenerateFieldAssignment()
appending field assignments after the record constructor body.

This change adjusts CompactConstructorDeclaration to not generate field
assignments if the execution flow is unreachable or dead after adding
the statements from the record constructor. This prevents generating
code after an athrow statement in the byte code.

Fixes: eclipse-jdt#487
Signed-off-by: Simeon Andreev <[email protected]>
trancexpress added a commit to trancexpress/eclipse.jdt.core that referenced this issue Mar 15, 2023
The following code results in generating instructions after an athrow
byte code statement:

record X(String s) {
    X {
        throw new RuntimeException();
    }
}

This is due to
CompactConstructorDeclaration.checkAndGenerateFieldAssignment()
appending field assignments after the record constructor body.

This change adjusts CompactConstructorDeclaration to not generate field
assignments if the execution flow is unreachable or dead after adding
the statements from the record constructor. This prevents generating
code after an athrow statement in the byte code.

Fixes: eclipse-jdt#487
Signed-off-by: Simeon Andreev <[email protected]>
trancexpress added a commit to trancexpress/eclipse.jdt.core that referenced this issue Mar 15, 2023
The following code results in generating instructions after an athrow
byte code statement:

record X(String s) {
    X {
        throw new RuntimeException();
    }
}

This is due to
CompactConstructorDeclaration.checkAndGenerateFieldAssignment()
appending field assignments after the record constructor body.

This change adjusts CompactConstructorDeclaration to not generate field
assignments if the execution flow is unreachable or dead after adding
the statements from the record constructor. This prevents generating
code after an athrow statement in the byte code.

Fixes: eclipse-jdt#487
Signed-off-by: Simeon Andreev <[email protected]>
@Marcono1234
Copy link
Author

Marcono1234 commented Mar 15, 2023

Possibly the fix is as simple as changing the addition order

That would probably violate the language specification:

After the last statement, if any, in the body of the compact constructor has completed normally (§14.1), all component fields of the record class are implicitly initialized to the values of the corresponding formal parameters.

The question would be whether that difference is really noticeable, but there might be contrived cases, such as leaking this in the canonical constructor, where this difference might have an effect.

Either way, since you did not chose this approach for your pull request it does not matter much.
Also thanks for fixing this!

@trancexpress
Copy link
Contributor

That would probably violate the language specification

Thanks for the hint!

@trancexpress trancexpress added compiler Eclipse Java Compiler (ecj) related issues bug Something isn't working labels Mar 17, 2023
trancexpress added a commit to trancexpress/eclipse.jdt.core that referenced this issue Mar 30, 2023
The following code results in generating instructions after an athrow
byte code statement:

record X(String s) {
    X {
        throw new RuntimeException();
    }
}

This is due to
CompactConstructorDeclaration.checkAndGenerateFieldAssignment()
appending field assignments after the record constructor body.

This change adjusts CompactConstructorDeclaration to not generate field
assignments if the execution flow is unreachable or dead after adding
the statements from the record constructor. This prevents generating
code after an athrow statement in the byte code.

Fixes: eclipse-jdt#487
Signed-off-by: Simeon Andreev <[email protected]>
iloveeclipse pushed a commit that referenced this issue Mar 30, 2023
The following code results in generating instructions after an athrow
byte code statement:

record X(String s) {
    X {
        throw new RuntimeException();
    }
}

This is due to
CompactConstructorDeclaration.checkAndGenerateFieldAssignment()
appending field assignments after the record constructor body.

This change adjusts CompactConstructorDeclaration to not generate field
assignments if the execution flow is unreachable or dead after adding
the statements from the record constructor. This prevents generating
code after an athrow statement in the byte code.

Fixes: #487
Signed-off-by: Simeon Andreev <[email protected]>
@trancexpress trancexpress self-assigned this Mar 30, 2023
@trancexpress trancexpress added this to the 4.28 M1 milestone Mar 30, 2023
robstryker pushed a commit to robstryker/eclipse.jdt.core that referenced this issue Jul 18, 2024
The following code results in generating instructions after an athrow
byte code statement:

record X(String s) {
    X {
        throw new RuntimeException();
    }
}

This is due to
CompactConstructorDeclaration.checkAndGenerateFieldAssignment()
appending field assignments after the record constructor body.

This change adjusts CompactConstructorDeclaration to not generate field
assignments if the execution flow is unreachable or dead after adding
the statements from the record constructor. This prevents generating
code after an athrow statement in the byte code.

Fixes: eclipse-jdt#487
Signed-off-by: Simeon Andreev <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working compiler Eclipse Java Compiler (ecj) related issues
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants