A prototype that transforms primitive like classes to civilized classes by rewriting the bytecode.
This repository contains two rewriters, one (ValueRewriter
) transforms classical Java object into value class
and another (ParametricRewriter
) transforms generics to support parametric generics
(so a list of values is fully specialized).
@Value record Person(@NonNull String name, @Nullable Address address, @NonNull Age age) {}
@Value record Address(@NonNull String address) {}
@Value @ImplicitlyConstructible record Age(int age) {}
The prototype uses 2 specific annotations and 4 nullabilty related annotations from jspecify.org,
- at declaration site, @Value declares a type with no identity (a value type),
- at declaration site, @ImplicitlyConstructible declares that the default value of the value type has all its fields fill with zeroes,
- at use site, org.jspecify.annotations.@NonNull declares that null is not a possible value,
- at use site, org.jspecify.annotations.@Nullable declares that null is a possible value (this is also the default if @NullMarked is not used).
On stack, value types are scalarized, on heap, non-null implcitly constructible value type are flattened, i.e. stored without a pointer.
For example, the field age declared below is stored as an int on heap because the value type Age
is declared
as @ImplicitlyConstructible and the field age
is declared @NonNull.
@NullMarked // implcitly non-null
class AgeContainer {
Age age; // non-null, must be initialized in the constructor or implicitly constructible
...
}
...
var container = new AgeContainer();
System.out.println(container.age); // Age[age=0]
Otherwise, a value type (implicitly constructible or not) works like any object.
var person = new Person("Bob", new Address("pont-aven"), new Age(16));
System.out.println(person.name); // Bob
System.out.println(person.address); // pont-aven
System.out.println(person.age); // Age[age=16]
Compared to a classical @NonNull, this one has some teeth, when declared on a parameter of a method, a nullcheck is performed for each call. When declared on a field only non-null implicitly constructible is enforced (because in the other case null is a valid default value when the field is not yet initialized).
This rewriter allows to declare new constant pool constants (that starts with $P
) and to specialize some operations/opcodes.
To specify a new constant, the rewriter recognize static final String
that starts with $P
and
transforms them to constant (constant dynamic constant).
Each constant is initialized with a kind of LISP (condy-LISP) that recognizes the instructions:
anchor
<ref> <parent-ref?>, parameters of the parametric class/method,array
<ref>, creates an array type,eval
<mh> <args<..., evaluate a method handle with arguments,linkage
<ref> specifies a parameters of one of the parametrized opcodes,list
<args>... creates a list,list.get
<list> <index> extracts the nth item of a list,mh
<class> <name> <descriptor> <constArgs...> creates a method handle (the constant arguments are inserted at the end),restriction
<refs...> specifies the classes of the method parameters/field type,species
<raw> <arg> creates a species,species.raw
<species> returns the raw part of a species,species.parameters
<species> returns the parameters part of a species,super
<species>... specifies the parametrized supers interfaces.
The atoms of a condy-LISP expression are
- a primitive type (Z, B, C, S, I, J, F, D) or void (V),
- a type descriptor, starts with 'L' , ends with a semicolon,
- a zero default value type descriptor, starts with 'Q', ends with a semicolon,
- a method type descriptor, starts with '(',
- a reference to another constant pool entry, starts with a 'P', ends with a semicolon,
- a string, starts with a quote ('),
- an integer, starts with a digit,
- a double, starts with a digit and has a dot (.) in the middle.
Runtime objects used by condy-LISP:
- Linkage: specify the type parameters for
the opcodes
new
,anewarray
,invokespecial
,invokevirtual
,invokeinterface
andinvokestatic
. - Restriction: specify the class of each method parameters.
- Species: a pair raw Class + an argument (arguments).
- Super: specify the super species (superclass + interfaces with their parameters).
To specify the linkage of a specialized operation, the rewriter recognize the pattern
"ref".intern();
operation
with ref a reference to a $P constant (with no semicolon at the end) and the operation one of the operation above.
In the following code, we first declare the class SimpleList
as parametric using the annotation @Parametric
,
the parameter P1 means that the lambda will be executed on the parameters (here to erase the parameters).
We then extract the argument of the class (in $KP0
) (or use the java/lang/Object if not defined) and
extract the zeroth argument (in $KP1
).
In the constructor, the linkage KP2
is used to specialize the array creation.
@Parametric("P1")
class SimpleList<E> {
private static final String $P0 = "list Ljava/lang/Object;";
private static final String $P1 = "mh Lcom/github/forax/civilizer/vm/JDK; 'erase (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; P0;";
private static final String $P2 = "mh Lcom/github/forax/civilizer/vm/JDK; 'erase (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; P0;";
private static final String $P3 = "anchor P1;";
private static final String $P4 = "list.get P3; 0";
private static final String $P5 = "linkage P4;";
private static final String $P6 = "restriction P4;";
private static final String $P7 = "anchor P2;";
private static final String $P8 = "linkage P7;";
private E[] elements;
private int size;
@SuppressWarnings("unchecked")
public SimpleList() {
super();
"P5".intern();
elements = (E[]) new Object[16]; // anewarray is specialized by P5
}
@TypeRestriction("P6")
public void add(E element) {
if (size == elements.length) {
elements = Arrays.copyOf(elements, elements.length << 1);
}
elements[size++] = element;
}
...
}
To use the SimpleList
defined above, we need to specialize the creation (new
) by providing a linkage
saying that the type argument is the class Complex
.
private static final String $P0 = "list.of Qcom/github/forax/civilizer/value/Complex;";
private static final String $P1 = "linkage P0;";
public static void main(String[] args) {
"P1".intern();
var list = new SimpleList<Complex>(); // new is specialized by P1
list.add(Complex.of(2.0, 4.0));
var element = list.get(0);
System.out.println(element);
}
All the specialized operations that takes a Linkage as parameter are re-written as invokedynamic calls with the correspondant constant pool constant as parameter. The values of the type arguments inside the constant pools are computed when asked.
There are more examples in the tests
Parametric class instantiation works, static and instance parametric methods instantiation works,
specialization of super (with @SuperType
), interfaces, and default methods works,
array specialization works, use site method specialization works,
raw types are supported (using the bsm referenced by the annotation @Parametric
).
Type restriction (with @TypeRestriction
) on fields and methods are implemented
(specialization of field storage is not implemented !).
You need the latest early access build of Valhalla jdk.java.net/valhalla/ and then launch maven
export JAVA_HOME=/path/to/jdk
mvn package
It will compile, rewrite the bytecode of any packages containing either value
or parametric
and run the tests.
The simple way is to check the tests and add new ones :)