Home / TeaVM Patterns and Pitfalls / TeaVM Patterns and Pitfalls     frequal.com
Page 1
1/62
TeaVM: translation difficulties from
Java to JavaScript
Alexey Andreev | Delightex
konsoletyper@gmail.com

Page 2
2/62
Speaker
Alexey Andreev
Compiler enthusiast
Worked for JetBrains for two years, contributed to Kotlin / JS
Before JetBrains, he worked in an enterprise, including fullstack-
developer
Now I work at Delightex

Page 3
3/62
TeaVM
AOT Java Bytecode Compiler
The most famous and stable backend is JavaScript
There is WebAssembly and C
Appeared in 2013 as a hobby project
Experience with GWT led to the idea: is it possible to translate bytecode
instead of sources
From spring 2019 production ready

Page 4
4/62
Analogs
GWT: Google's most famous Java to JavaScript compiler
J2CL: Google's project to replace GWT
Kotlin / JS: Kotlin to JS compiler by JetBrains
Scala.js: Scala compiler to JS
CheerpJ: Browser Java Virtual Machine
Etc.

Page 5
5/62
Who is used
CoSpaces (wanted to start migrating to Kotlin, GWT did not allow)
Codename One (wanted streams like in Java)
Greenfoot (wanted streams like in Java)
Perhaps someone else

Page 6
6/62
CoSpaces
https://edu.cospaces.io/

Page 7
7/62
Lost in translation
Java and JavaScript aren't all that alike
The devil is in the details
Emulating Java features in JavaScript
Get completely idiomatic JavaScript code
impossible

Page 8
8/62
Overload by signature
static void foo (String value) {
System. out .println ( "string" );
}
static void foo (Integer value) {
System. out .println ( "integer" );
}

Page 9
9/62
Overload by signature
function foo (value) {
switch ( typeof value) {
case "string" :
console . log ( "string" );
break ;
case "number" :
console . log ( "integer" );
break ;
}
}

Page 10
10/62
Overload by signature
static void foo (String value) {
System. out .println ( "string" );
}
static void foo (Integer value) {
System. out .println ( "integer" );
}
static void bar () {
foo ((String) null );
foo ((Integer) null );
}

Page 11
11/62
Overload by signature
static void foo (String value) {
System. out .println ( "string" );
}
static void foo (Integer value) {
System. out .println ( "integer" );
}
static void bar () {
foo ((String) null );
foo ((Integer) null );
}
How to distinguish these nulls on
runtime?

Page 12
12/62
Overload by signature
function foo_1 (value) {
return "string" ;
}
function foo_2 (value) {
return "integer" ;
}
function bar () {
foo_1 ( null );
foo_2 ( null );
}
We distinguish them in compile time

Page 13
13/62
<clinit>
class A {
static int FOO = bar ();
static int bar () {
return 23 ;
}
}
class Test {
public static void main (String [] args) {
System. out .println (A. FOO );
}
}

Page 14
14/62
<clinit>
class A {
static bar () {
return 23 ;
}
}
A. FOO = A. bar ();
console . log (A. FOO );

Page 15
15/62
<clinit>
class A {
static int FOO = bar ();
static int bar () {
System. out .println ( "bar" );
return 23 ;
}
}
class Test {
public static void main (String [] args) {
System. out .println ( "start" );
System. out .println (A. FOO );
}
}
start
bar
23

Page 16
16/62
<clinit>
A special static method that javac inserts into
classes with static fields with initializers
Called when the class is initialized in the following cases
-
when calling static method
-
when reading or modifying a static field
-
on instantiation
-
when calling Class.forName
Of course, only once
In JavaScript, classes don't have initializers

Page 17
17/62
<clinit>
class A {
static bar () {
console . log ( "bar" );
return 23 ;
}
static clinit () {
A. clinit = () => {};
A. FOO = bar ();
}
}
console . log ( "start" );
A.clinit ();
console . log (A. FOO );

Page 18
18/62
equals / hashCode
class A {
int value ;
A ( int value) {
this . value = value;
}
public static void main (String [] args) {
var set = new HashSet <A> ();
set.add ( new A ( 23 ));
set.add ( new A ( 23 ));
set.add ( new A ( 42 ));
}
}

Page 19
19/62
equals / hashCode
class A {
constructor (value) {
this . value = value;
}
}
var set = new Set ();
set . add ( new A ( 23 ));
set . add ( new A ( 23 ));
set . add ( new A ( 42 ));

Page 20
20/62
equals / hashCode
class A {
int value ;
A ( int value) {
this . value = value;
}
public boolean equals (Object that) {
return value == ((A) that). value ;
}
public int hashCode (Object that) { return value ; }
static void main (String [] args) {
var set = new HashSet <A> ();
set.add ( new A ( 23 ));
set.add ( new A ( 23 ));
set.add ( new A ( 42 ));
}
}

Page 21
21/62
equals / hashCode
In JS, there is no equals / hashCode in the Object class, respectively
collections don't know anything about it
Way out: define your base class for all Java classes
(for example, jl_Object), but then we have problems with interop
Alternative (GWT, Kotlin / JS): on every call to equals / hashCode
we generate code that finds out the type of object and applies
the logic you want (bad for performance)
You also need to write your own implementation of collections

Page 22
22/62
ArrayList
class A {
public static void main (String [] args) {
var list = new ArrayList <> (List. of ( 2 , 3 , 5 , 7 , 11 ));
for ( var elem: list) {
foo (elem, list);
}
}
static void foo ( int elem, List <Integer> list) {
System. out .println (elem + "of" + list);
}
}

Page 23
23/62
ArrayList
var list = [ 2 , 3 , 5 , 7 , 11 ];
for ( var elem of list ) {
foo ( elem , list );
}
function foo (elem, list) {
console . log ( ` $ {elem} of $ {list} ` );
}

Page 24
24/62
ArrayList
class A {
public static void main (String [] args) {
var list = new ArrayList <> (List. of ( 2 , 3 , 5 , 7 , 11 ));
for ( var elem: list) {
foo (elem, list);
}
}
static void foo ( int elem, List <Integer> list) {
System. out .println (elem + "of" + list);
if (elem == 2 ) list.add ( 13 );
}
}

Page 25
25/62
ArrayList
var list = [ 2 , 3 , 5 , 7 , 11 ];
for ( var elem of list ) {
foo ( elem , list );
}
function foo (elem, list) {
console . log ( ` $ {elem} of $ {list} ` );
if ( elem === 2 ) list . push ( 13 );
}

Page 26
26/62
ArrayList
2 of 2,3,5,7,11
3 of 2,3,5,7,11,13
5 of 2,3,5,7,11,13
7 of 2,3,5,7,11,13
11 of 2,3,5,7,11,13
13 of 2,3,5,7,11,13
2 of [2, 3, 5, 7, 11]
Exception in thread "main" java.util.ConcurrentModificationException
at java.base / java.util.ArrayList $ Itr.checkForComodification (ArrayList.java:1042)
at java.base / java.util.ArrayList $ Itr.next (ArrayList.java:996)
at A.main (A.java:7)

Page 27
27/62
Compiler
What to compile
Optimization
Invokedynamic
We get AST
Green threads

Page 28
28/62
What to compile
JavaScript should be as small as possible
We only compile the code that really will be
be performed
Class dependency graph
A class with a main method is reachable
Let A be reachable
Let B be mentioned in A
Hence B is also reachable

Page 29
29/62
What to compile
class A {
public static void main (String [] args) {
B. foo ();
}
}
class B {
static void foo () {
System. out .println ( "I'm B.foo" );
}
static void bar () {
C. baz ();
}
}
A
main
B
bar
C

Page 30
30/62
What to compile
bar is not used, but we compile it
bar pulls a whole class C with it
So, we traverse the graph of methods
The main method is reachable
Let method a be reachable
Let b be called in method a
Then b is also reachable
In this case, only A.main and B.foo are reachable
Problem: virtual calls

Page 31
31/62
What to compile
class A {
public static void main (String [] args) {
for (J instance: instances ()) {
instance.foo ();
}
}
static List <J> instances () {
return Arrays. asList ( new B (), new C ());
}
}

Page 32
32/62
What to compile
class A {
public static void main (String [] args) {
for (J instance: instances ()) {
instance.foo ();
}
}
static List <J> instances () {
return Arrays. asList ( new B (), new C ());
}
}
⟵ what is instance.foo?

Page 33
33/62
What to compile
class A {
public static void main (String [] args) {
for (J instance: instances ()) {
instance.foo ();
}
}
static List <J> instances () {
return Arrays. asList ( new B (), new C ());
}
}
new: ∅
A.main
A.instances
J.foo

Page 34
34/62
What to compile
class A {
public static void main (String [] args) {
for (J instance: instances ()) {
instance.foo ();
}
}
static List <J> instances () {
return Arrays. asList ( new B (), new C ());
}
}
new: {B, C}
A.main
A.instances
J.foo

Page 35
35/62
What to compile
class A {
public static void main (String [] args) {
for (J instance: instances ()) {
instance.foo ();
}
}
static List <J> instances () {
return Arrays. asList ( new B (), new C ());
}
}
new: {B, C}
A.main
A.instances
J.foo
B.foo
C.foo

Page 36
36/62
What to compile
Bonus: devirtualization
-
If a virtual method resolves to only one implementation,
we replace the virtual call of this method with a direct call
implementation
-
Direct method calls are faster than virtual calls
Bonus: call graph
-
Useful for some optimizations

Page 37
37/62
What to compile
Problem: reflection
-
Method.invoke - any method of any class can be called
-
Class.forName - can be any class at all
-
Way out: do not use reflection at all (annotation processors)
-
Way out: explicitly annotate methods that can be called via
reflection
There are stronger algorithms than the one suggested

Page 38
38/62
What to compile
class A {
public static void main (String [] args) {
for (J instance: instances ()) {
instance.foo ();
}
}
static List <J> instances () {
new B ();
return Arrays. asList ( new C ());
}
}

Page 39
39/62
What to compile
Obviously B.foo is no longer needed
You need to understand what instances return
This is data-flow analysis
Data-flow analysis is trying to understand something about the values ​​that
a variable can take at every point in the program
For example, what types can these values ​​have?
Knowing this at the point of virtual call, we can correctly resolve
method
SSA (static single assignment) is convenient for data-flow analysis

Page 40
40/62
SSA
var a = 1 ;
System. out .println (a);
a + = 1 ;
System. out .println (a);
Here you can substitute constants
How do you make this obvious to the machine?
a changes, so it is not a constant
Must work in terms of definitions, not variables
Or rewrite the code

Page 41
41/62
SSA
var a_1 = 1 ;
System. out .println (a_1);
var a_2 = a_1 + 1 ;
System. out .println (a_2);
What happened - SSA
SSA is such an intermediate representation (intermediate
representation, IR)
In SSA, each variable is assigned only once
Now you can drag in constants in an obvious way
SSA is used in any normal compiler

Page 42
42/62
Optimization
Global value numbering
Loop invariant code motion
Dead code elimination
Redundant null check elimination
Class initialization elimination
Scalar replacement
Inlining
Devirtualization
Eager class initialization

Page 43
43/62
invokedynamic
Invokedynamic is to some extent reflection
There are invokedynamic that we know how they work
If the bytecode contains unknown invokedynamic, then we swear at it
Luckily javac only generates known invokedynamic
LambdaMetafactory:
-
generating the class at compile time
-
replace invokedynamic with an instance of this class
StringConcatFactory: replace with StringBuilder.append

Page 44
44/62
We get AST
static int max (Node node) {
var result = Integer. MIN_VALUE ;
while (node! = null ) {
if (node.value> result) {
result = node.value;
}
node = node.next;
}
return result;
}

Page 45
45/62
We get AST
AST, abstract syntax tree, abstract syntax tree
Compilers parse text into AST
AST is then analyzed
AST turns into bytecode
In TeaVM, the opposite is true

Page 46
46/62
We get AST
;
=
while
return
result
Integer.MIN_VALUE
! =
;
node
null
if
=
>
=
node.value
result
result
node.value
node
node.next
result

Page 47
47/62
We get AST
$ start
@ 2 = -2147483648
goto $ loop
$ loop
if @node == null then goto $ exit else goto $ body
$ body
@ 3 = field Node.value @node as I
@ 4 = @ 2 compareTo @ 3 as int
if @ 4 <0 then goto $ less else goto $ greater
$ less
@ 2 = field Node.value @node as I
goto $ greater
$ greater
@node = field Node.next @node as `LNode;`
goto $ loop
$ exit
return @ 2

Page 48
48/62
We get AST
Let the IR piece between two marks be
block
Let's draw an arrow from block A to block B if
there is a transition from block A to block B (conditional or
unconditional)
Got CFG (control-flow graph, flow graph
management)
CFG is used for compilers, static
analyzers, IDEs, etc.
Task - convert CFG to AST
start
loop
body
exit
less
greater

Page 49
49/62
We get AST
CFG: reducible and irreducible
Irreducible graphs are graphs with cycles in which
there is more than one entrance
Irreducible graphs are obtained only from async-
methods, coroutines, generators
They can be converted into reducible (expensive)
You can always restore AST for those
enter
A
C
B
D
exit

Page 50
50/62
We get AST
In the picture with the graph, the arrow leading to the beginning
cycle pointing up
Everyone else is down
This graph can always be drawn
The up arrows are while (true)
Down arrows is a break from a block with a label
We optimize
start
loop
body
exit
less
greater

Page 51
51/62
We get AST
function Node_max ($ node) {
var $ result;
$ result = (- 2147483648 );
loop: while ( true ) {
if ($ node === null ) break loop;
greater: {
less: {
if ($ node. $ value <= $ result) {} else break less;
break greater;
} // less
$ result = $ node. $ value;
break greater;
} // greater
$ node = $ node. $ next;
continue loop;
}
return $ result;
}
start
loop
body
exit
less
greater

Page 52
52/62
We get AST
function Node_max ($ node) {
var $ result ;
$ result = (- 2147483648 );
while ($ node! == null ) {
if ($ node. $ value0> $ result )
$ result = $ node. $ value0;
$ node = $ node. $ next;
}
return $ result ;
}
function Cu (b) {var c; c = (- 2147483648); while (b! == null)
{if (br> c) c = br; b = bL;} return c;}

Page 53
53/62
Threads
WebWorkers are not threads, they are analogous to processes
Threads from the UI are often launched to prevent heavy calculations
hung up the UI
Heavy Computing: CPU-bound, IO-bound
Nothing can be done about CPU-bound
But IO-bound can be resolved using green threads
Green threads are threads that are machine driven and not
OS
In our case, the OS is a browser

Page 54
54/62
Threads
Coroutine (coroutine, coroutine) is a procedure that
can return to the calling procedure without completing
work to the end
The calling procedure can continue execution
coroutines or not
It is not necessary to continue immediately, you can postpone the execution
for later
For example, give programs to the scheduler
Using a call graph to infect a program with async

Page 55
55/62
Threads
System. out .println ( "before" );
Thread. sleep ( 1 );
System. out .println ( "after" );

Page 56
56/62
Threads
$ ptr = 0 ;
if ($ rt_resuming ()) {
var $ thread = $ rt_nativeThread ();
$ ptr = $ thread . pop (); var $ 1 = $ thread . pop ();
}
main : while ( true ) { switch ( $ ptr ) {
case 0 :
jl_System_out (). $ println ($ rt_s ( 12 ));
var $ 1 = Long_fromInt ( 1 );
$ ptr = 1 ;
case 1 :
jl_Thread_sleep ( var $ 1 );
if ($ rt_suspending ()) break main ;
jl_System_out (). $ println ($ rt_s ( 13 ));
return ;
}}
$ rt_nativeThread (). push ( var $ 1 , $ ptr );

Page 57
57/62
Threads
$ ptr = 0 ;
if ($ rt_resuming ()) {
var $ thread = $ rt_nativeThread ();
$ ptr = $ thread . pop (); var $ 1 = $ thread . pop ();
}
main : while ( true ) { switch ( $ ptr ) {
case 0 :
jl_System_out (). $ println ($ rt_s ( 12 ));
var $ 1 = Long_fromInt ( 1 );
$ ptr = 1 ;
case 1 :
jl_Thread_sleep ( var $ 1 );
if ($ rt_suspending ()) break main ;
jl_System_out (). $ println ($ rt_s ( 13 ));
return ;
}}
$ rt_nativeThread (). push ( var $ 1 , $ ptr );
We restore
state

Page 58
58/62
Threads
$ ptr = 0 ;
if ($ rt_resuming ()) {
var $ thread = $ rt_nativeThread ();
$ ptr = $ thread . pop (); var $ 1 = $ thread . pop ();
}
main : while ( true ) { switch ( $ ptr ) {
case 0 :
jl_System_out (). $ println ($ rt_s ( 12 ));
var $ 1 = Long_fromInt ( 1 );
$ ptr = 1 ;
case 1 :
jl_Thread_sleep ( var $ 1 );
if ($ rt_suspending ()) break main ;
jl_System_out (). $ println ($ rt_s ( 13 ));
return ;
}}
$ rt_nativeThread (). push ( var $ 1 , $ ptr );
Go to Thread.sleep

Page 59
59/62
Threads
$ ptr = 0 ;
if ($ rt_resuming ()) {
var $ thread = $ rt_nativeThread ();
$ ptr = $ thread . pop (); var $ 1 = $ thread . pop ();
}
main : while ( true ) { switch ( $ ptr ) {
case 0 :
jl_System_out (). $ println ($ rt_s ( 12 ));
var $ 1 = Long_fromInt ( 1 );
$ ptr = 1 ;
case 1 :
jl_Thread_sleep ( var $ 1 );
if ($ rt_suspending ()) break main ;
jl_System_out (). $ println ($ rt_s ( 13 ));
return ;
}}
$ rt_nativeThread (). push ( var $ 1 , $ ptr );
Interrupt
execution and
save
condition if
need to

Page 60
60/62
Threads
$ ptr = 0 ;
if ($ rt_resuming ()) {
var $ thread = $ rt_nativeThread ();
$ ptr = $ thread . pop (); var $ 1 = $ thread . pop ();
}
main : while ( true ) { switch ( $ ptr ) {
case 0 :
jl_System_out (). $ println ($ rt_s ( 12 ));
var $ 1 = Long_fromInt ( 1 );
$ ptr = 1 ;
case 1 :
jl_Thread_sleep ( var $ 1 );
if ($ rt_suspending ()) break main ;
jl_System_out (). $ println ($ rt_s ( 13 ));
return ;
}}
$ rt_nativeThread (). push ( var $ 1 , $ ptr );
We execute the code after
Thread.sleep

Page 61
61/62
conclusions
TeaVM is production-ready
TeaVM can be used if your cross-platform
Java code needs to support one more platform
TeaVM uses JavaScript as an assembler
TeaVM optimizes
Writing your own AOT bytecode compiler is not difficult; complicated
write a good AOT bytecode compiler

Page 62
62/62
Thanks for your attention!
Alexey Andreev | Delightex
konsoletyper@gmail.com

Original text