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.

maybe statement ast

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.

maybe resulting ast

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: