sebastiandaschner blog
Making Java esoteric — adding a maybe
keyword
wednesday, october 07, 2015
Today we will hack on the Java language and the Java Compiler itself.
As the execution of Java code is pretty deterministic, let’s add a maybe
keyword to the language which maybe executes certain statements.
The desired code may look like follows:
public class Hello {
public static void main(final String... ignored) {
String string = "hello world";
maybe {
string = "goodbye world";
System.out.println(string + '!');
}
System.out.println(string);
maybe System.out.println(string);
}
}
This would then return hello world
or maybe goodbye!
.
In order to implement such a language feature we will have to modify the Java Compiler. The sources Java’s OpenJDK project are open source and available via http://hg.openjdk.java.net/jdk9. We will take the latest JDK9 sources for these examples.
The JDK is build via make.
The sources of the Compiler are found under langtools
.
Step-by-step:
hg clone http://hg.openjdk.java.net/jdk9/jdk9 cd jdk9/ chmod +x get_source.sh ./get_source.sh make all cd build/<your-build>/jdk/bin/ ./javac <some_java_source>
The Java binaries are placed under build/<your-build>/jdk/bin
.
In order to introduce the new maybe
keyword to the compiler we will need to modify some components.
First of all the tokens which are identified by the scanner have to be extended.
From src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Tokens.java
:
...
INTERFACE("interface"),
LONG("long", Tag.NAMED),
MAYBE("maybe"), // this one is new
NATIVE("native"),
NEW("new"),
...
This modification will make the scanner detect the maybe
words no longer as regular identifier rather than Java keyword (own token in the compiler).
The parser then has to know how to handle that new token.
As there is no maybe or random bytecode instruction we will need to simulate the randomness of a statement in other ways.
The parser constructs the AST (Abstract Syntax Tree) from the tokenstream. In order to randomly execute a statement or block we would need to encapsulate the block with an conditional structure.
This if-condition should execute the statement randomly, therefore we might insert some call like new Random().nextBoolean()
.
This wrappes the statement or block and randomly executes it or not.
Having that said the bytecode of our maybe
statement looks like as if the block has been wrapped with that call:
maybe {
System.out.println("hello");
}
will produce the same bytecode as
if (new Random().nextBoolean()) {
System.out.println("hello");
}
This means we will create the corresponding AST structure for the maybe
statement.
In src/[…]/parser/JavacParser.java
's blockStatement()
method:
...
case CATCH:
case ASSERT:
case MAYBE:
return List.of(parseSimpleStatement());
case MONKEYS_AT:
...
And in the parseSimpleStatement()
method:
...
case MAYBE: {
nextToken();
JCStatement body = parseStatementAsBlock();
return F.at(pos).Maybe(body);
}
case TRY: {
...
This code will read the MAYBE
token and expects the next tokens to be handled as a statement or block, respectively.
The Maybe(body)
call of the parser.TreeMaker
class will construct the If tree as desired:
public JCIf Maybe(JCStatement body) {
// construct if with expression `new Random().nextBoolean()` around body
JCExpression javaUtil = new JCFieldAccess(new JCIdent(names.fromString("java"), null), names.fromString("util"), null);
JCExpression randomType = new JCFieldAccess(javaUtil, names.fromString("Random"), null);
JCExpression randomInst = new JCNewClass(null, List.nil(), randomType, List.nil(), null);
JCExpression methodRef = new JCFieldAccess(randomInst, names.fromString("nextBoolean"), null);
JCMethodInvocation methodInvocation = new JCMethodInvocation(List.nil(), methodRef, List.nil());
JCIf tree = new JCIf(methodInvocation, body, null);
tree.pos = pos;
return tree;
}
That’s all we need to change. Now the project can be rebuild using ant and we can use our new language feature!
public class Hello {
public static void main(final String... ignored) {
String string = "hello world";
maybe {
string = "goodbye world";
System.out.println(string + '!');
}
System.out.println(string);
maybe System.out.println(string);
}
}
build/<your-build>/jdk/bin/javac Hello.java java Hello # hello world # hello world java Hello # hello world java Hello # goodbye world! # goodbye world java Hello # goodbye world! # goodbye world # goodbye world
Update (2016-02-21): Changed build instructions to match latest JDK9 version.
Found the post useful? Subscribe to my newsletter for more free content, tips and tricks on IT & Java: