tweag/inline-java

support inner classes in inline-java

Closed this issue · 7 comments

Currently inline-java fails with a runtime error when trying to marshal an object of an inner class.

[java| Thread.State.NEW |] :: IO (J ('Class "java.lang.Thread.State"))

produces

Exception in thread "main" java.lang.NoSuchMethodError: inline__method_25

There are two problems with this behavior.

  1. Marshaling these objects should be possible.
  2. Ideally, inline-java should not produce these runtime errors when the user types an incorrect type.

To fix (1) we can have the user be explicit in that Thread.State is an inner class. He could do so by writing instead:

[java| Thread.State.NEW |] :: IO (J ('Class "java.lang.Thread:State"))

using : as a separator of the inner class. We can't use $ as a separator because $ is a valid character in Java identifiers.

To fix (2), the plugin could call javap on the generated bytecode and check that the signature of the methods inline__method_i are as expected. Thus, typing . instead of : would be caught at build time.

mboes commented

SGTM.

mboes commented

I do wonder though, is it really such a cost to just assume that dollar signs always refer to inner classes? I know that technically the user is allowed to call his/her classes Test$, but it sounds fair to ban that in inline-java. The flip side is that in doing so we avoiding inventing new bespoke and non-standard syntax that the user has to learn.

mboes commented

Furthermore, the Java Language Specification says this:

The "Java letters" include uppercase and lowercase ASCII Latin letters A-Z (\u0041-\u005a), and a-z (\u0061-\u007a), and, for historical reasons, the ASCII underscore (_, or \u005f) and dollar sign ($, or \u0024). The $ sign should be used only in mechanically generated source code or, rarely, to access pre-existing names on legacy systems.

https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8

We can ask the user to not use $, but he still might have to use classes whose names he hasn't chosen.

We could consider adding some escape syntax instead. So a.b$c is interpreted as a.b.c, but a.b\$c, a.b$$c or a.b:c is interpreted as a.b$c.

mboes commented

but he still might have to use classes whose names he hasn't chosen.

That's easy enough to work around (with class aliases), and if we are to take the JLS on face value, entirely a theoretical problem (i.e. only applicable for very old legacy code). And yes, we could introduce an escaping mechanism for legacy names, but I'd wait to see whether someone needs that. At least this way, the default (i.e. when we're not dealing with legacy names) is the unsurprising case, and the user only needs to do something special when dealing with legacy things (that may well mean never in this day age, but I'm not a Java old timer).

Regarding (2), we can do better than checking javap. In java we can test if the a class name has the name we expected

A.B.C.class.getName().equals("A.B$C")

We write A.B.C.class using only dots, and "A.B$C" comes from the type of the java references as written by the user. If this test fails, we can report an error.

This test can be performed at runtime by inserting it in static blocks in the generated classes, or it can be done at build time in a few ways. The simplest way I found to do it at build time is to call javac on a generated program which runs the test. It takes near (80 ms). This time could be reduced if we hook javac to run the tests for us. javac offerst a couple of mechanisms for this (the Annotation Processor API and plugins).

I propose that we start with the simple approach to check it at build time. If 80 ms turns out to be too expensive, we either do it at runtime or consider javac hooks.

The main advantage of this approach is that we get better error messages. The user does not have to spend time figuring out the cause of a NoSuchMethodError message.

Regarding (2), we can do better than checking javap.

This turned out to complicate the plugin more than what a sparingly-used feature would deserve.

We can revisit this if inner classes are used more than we anticipate. The patches that we tried before are here.