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