“Everything starts with a class in Java. Stick that in a text file, and go get your IDE or Makefile or whatever set up. I’ll be right here when you’re ready. Good? OK!” - Crafting Interpreters, 4.1 - The Interpreter Framework

“Or Whatever Setup”

Ok so we’re reading Crafting Interpreters, and the book wants to write its examples in Java. There are a lot of ways you could go about building and executing a Java project. Since I am a masochist who refuses to let an IDE set up my project for me, I’ll need to choose that setup for myself.

One could, like the cavemen of olden days, just use javac to build her code, and java to run it. Go ahead and try this if you like, I do not intend to go into detail about why this is tedious. Besides the fact that simply building and executing your programs with the java executables is less straightforward than one might prefer, there are some additional benefits to using a build tool. I personally do not want to work on a project of any size without the ability to write tests, and writing tests usually entails dependencies. A few chapters in, the book has you write a code generation tool to get around Java’s tragic verbosity, and its nice to have a task runner and a separate build pipeline to deal with this tool. I’ll explain how I used Gradle to do each of these things.

Gradle vs Maven vs Ant

There are a few different popular build tools to consider when starting a new Java project, and I spent almost zero time evaluating them. I decided to use Gradle because:

  1. Gradle is the standard tool for building Android projects
  2. Gradle uses Kotlin (or Groovy) instead of XML as the configuration language
  3. I have used Gradle before

Are there good reasons to choose one of the others? Maybe!

Getting Started

Install java, install gradle

I won’t go into detail here because this is system dependent. I used my system package manager to install java 21 and the global gradle that you’ll use to initialize your project. If you need more assitance getting gradle installed, check this out: https://gradle.org/install/.

Create a new gradle project

Once you have gradle installed, you can get your project set up pretty easily with this command:

gradle init --package com.craftinginterpreters.lox
            --project-name lox
            --java-version 21
            --test-framework junit
            --dsl kotlin
            --type java-application
            --no-incubating
            --no-split-project

More about gradle init: https://docs.gradle.org/current/userguide/build_init_plugin.html

If you leave out all the options to gradle init, it will prompt you to make a selection for each of these.

  • package: Creates directory structure that matches the package name
  • project-name: Self explanatory. Not sure if this has any effect on anything other than just being the nanme of the project
  • java-version: The version of Java you want to use.
  • test-framework: By default it prompts you to choose “junit” which really means junit 4. That’s what I chose, but I was unaware that there is a junit 5, and you can select it here by choosing “jupiter-junit” instead. Since I did not find this out until later, I’ll use junit 4 for my examples in the next post, but it should be easy enough to translate to junit 5 if you prefer to use that instead.
  • dsl: You can choose to configure the project with Kotlin or Groovy. Since its plausible I may want to learn Kotlin in the future, I preferred to choose that. The downside is that it’s often easier to find examples online written in Groovy.
  • type: This allows you to pick a project template. java-appliation is “a command line application written in java” which is exactly what we want. More about the application plugin: https://docs.gradle.org/current/userguide/application_plugin.html
  • no-incubating: You can choose between the bleeding edge and stable versions of Gradle. I decided to go with the stable version since it was pretty likely that I would be going through this book on and off over a long period of time. I want the build to “just work” even if I haven’t touched it in 6 months.
  • no-split-project: This allows you to have multiple projects with separate build files. We don’t really need that for a project this small.

Once you’ve run gradle init configured to your liking, you should have java files named App.java and AppTest.java created inside this directory structure. I would change them both to Lox.java and LoxTest.java and rename the classes within. You’ll also want to checkout app/build.gradle.kts and change the main class to match. This will make it a little easier to follow along with the book which names its entrypoint Lox instead of App.

Check this out for more info about initializing Gradle: https://docs.gradle.org/current/userguide/build_init_plugin.html

Dependencies

You probably won’t need to add any more dependencies, since gradle init will install the test framework for you. If you updated the main class to Lox, your app/build.gradle.kts should look like this:

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java application project to get you started.
 * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.12/userguide/building_java_projects.html in the Gradle documentation.
 */

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    application
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit test framework.
    testImplementation(libs.junit)

    // This dependency is used by the application.
    implementation(libs.guava)
}

// Apply a specific Java toolchain to ease working on different environments.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

application {
    // Define the main class for the application.
    mainClass = "com.craftinginterpreters.lox.Lox"
}

You’ll notice that gradle added an additional dependency called guava. Guava is a kind of supplemental standard library, with lots of neat doodads and utilities. Code from the book won’t take advantage of that library, so you can go ahead and deleted the depenedency. You can read more about guava here if you are curious: https://github.com/google/guava

The Gradle Wrapper

gradle init creates a script called gradlew in the root of your project. This is the primary way you’ll interact with gradle. It will install the correct version of gradle that your project requires if its missing, and use that gradle to run your tasks. This makes it really easy to get set up on another machine.

Unit Tests

You can run your tests with ./gradlew test. I often run them with the --info flag which will allow exceptions thrown and System.out.println to get printed out in the console for you. I’ll discuss writing test cases in a future post.

Configuring tasks

You’ll primarily interact with gradle by running tasks. So far we’ve used tasks that come with gradle like init, and tasks that gradle defines for your project like run and test. The behavior of existing tasks can be configured to our liking. In addition we can create our own tasks.

Configuring run

java-application comes with a run task that executes our application. The jlox application will run accept input in two different forms:

  • From a .lox sources files
  • As a REPL accepting strings of lox code from standard input

By default gradle will not wire up standard input to your tasks, so we’ll need to enable that explicitly for the REPL to work properly. To test this out you can setup your Lox.java file to look like this:

/*
 * This source file was generated by the Gradle 'init' task
 */
package com.craftinginterpreters.lox;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Lox {
    public static void main(String[] args) throws IOException {
        System.out.println("Input string:");

              InputStreamReader input = new InputStreamReader(System.in);
              BufferedReader reader = new BufferedReader(input);

              String line = reader.readLine();
              System.out.println("Line:");
              System.out.println(line);
    }
}

If you try ./gradlew run without modifying the run task it should print null without waiting for your input.

Add this to your build.gradle.kts to configure the run task:

tasks.named<JavaExec>("run") {
    // Needed for the repl
    standardInput = System.`in`
}

Now try running that again. It should wait for your input now, but you’ll also see a bunch of “gradle stuff” that interrupts the input experience.

Cleaning up the command line output

A few command line arguments can clean up the output from gradle to give you a better experience interacting with the REPL:

./gradlew run -q --console=plain

Custom tasks

Later in the book we write a code generation tool. We can create custom task to run this code. You can create a placeholder file to test this out and put it in app/src/main/java/com/craftinginterpreters/tool/GenerateAst.java:

package com.craftinginterpreters.tool;

public class GenerateAst {
    public static void main(String[] args) {
        System.out.println("Generate AST");
    }
}

Then add this to your build.gradle.kts:

tasks.register<JavaExec>("generateAst") {
    classpath = sourceSets.named("tool").get().runtimeClasspath
    mainClass.set("com.craftinginterpreters.tool.GenerateAst")
}

You now run this tasks like so:

./gradlew app:generateAst

Passing command line arguments to your tasks

Crafting Interperters will instruct you to accept a directory name as a command line argument where you will output the generated code. Let’s go ahead and test that out too. Let’s tweak the code for GenerateAst.java a bit to print out the first command line arg you pass:

package com.craftinginterpreters.tool;

public class GenerateAst {
    public static void main(String[] args) {
        System.out.println("Generate AST to file: " + args[0]);
    }
}

And now you can pass an argument to your task like so:

./gradlew app:generateAst --args="$PWD/app/src/main/java/com/craftinginterpreters/lox"

Using sourceSets to isolate unrelated code

Ultimately Crafting Interpreters will instruct you to fill this file with code that outputs a new source file with interfaces you’ll import into the rest of your code. One problem you may run into is that if you add new methods to these interfaces, it will immediately break the rest of your code because you’ve failed to implement them yet. But what if you need to run app:generateAst again? app:generateAst shouldn’t really depend on the rest of the code, and indeed Gradle does give us a tool to deal with this called sourceSets.

The first thing we’re going to want to do is to move the tool directory out of app/src/main into its own top level directory app/src/tool. I tried as much as possible to avoid modifying the code in the book, but I think we’re fighting against the tides if we try to keep source code that should be isolated inside of main. Let’s move app/src/main/java/com/craftinginterpreters/tool/GenerateAst.java to app/src/tool/java/com/craftinginterpreters/tool/GenerateAst.java.

Add this sourceSets section and update your generateAst task in your build.gradle.kts file:

sourceSets {
    create("tool") {
        java {
            srcDir("src/tool")
        }
    }
}

tasks.register<JavaExec>("generateAst") {
    classpath = sourceSets.named("tool").get().runtimeClasspath
    mainClass.set("com.craftinginterpreters.tool.GenerateAst")
}

Now you can break whatever you want in the primary codebase without affecting the tool.

Pass command line arguments from the task configuration

A quick quality of life improvement is to configure the default command line arguments for generateAst right in the task configuration.

Update your task configuration like so:

tasks.register<JavaExec>("generateAst") {
    classpath = sourceSets.named("tool").get().runtimeClasspath
    mainClass.set("com.craftinginterpreters.tool.GenerateAst")
    args = listOf(project.file("app/src/main/java/com/craftinginterpreters/lox").getAbsolutePath())
}

Now you can just run:

./gradlew app:generateAst

And you retain the ability to pass the args explicitly if you want.

Task dependencies

Ok was that really necessary? You’ve got ctrl-r after all. We can build on this by making generateAst a task dependency for run and test. Now you don’t have to worry about forgetting to run generateAst at all. You can just run run and test and be confident that all of the generated code is in place.

Using dependsOn for task dependencies

The first thing I tried was to have run and test depend on generateAst directly. Unfortunately this approach has a problem. What we want is to run generateAst not just before run or test execute their code. We actually need it to run before run and test compile their code.

Gradle’s compilation tasks

It turns out, each SourceSet comes with its own set of compilation tasks. For the “main” SourceSet, that task is called “compileJava”. (For the tool SourceSet, the task gets a suffix: “compileJavaTool”. This is a common convention in gradle, where suffixes are omitted when things refer to the “main” SourceSet.) To make sure that the compilation of the “main” SourceSet is dependent on generateAst, let’s fix our build file like so:

val generateAst by tasks.register<JavaExec>("generateAst") {
    group = "build"
    description = "Generates source code using the AST tool"

    classpath = sourceSets.named("tool").get().runtimeClasspath
    mainClass.set("com.craftinginterpreters.tool.GenerateAst")
    args = listOf(project.file("src/main/java/com/craftinginterpreters/lox").getAbsolutePath())
}

tasks.named("compileJava") {
    dependsOn(generateAst)
}

Conceptually, this is a better approach anyway. The code we generate with generateAst should always be generated before will compile “main”, and any future tasks that depend on “main” will get to take advantage of our tool.

fin.

Here’s the finished build.gradle.kts file:

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java application project to get you started.
 * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.12/userguide/building_java_projects.html in the Gradle documentation.
 */

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    application
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit test framework.
    testImplementation(libs.junit)
}

// Apply a specific Java toolchain to ease working on different environments.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named<JavaExec>("run") {
    // Needed for the repl
    standardInput = System.`in`
    dependsOn("generateAst")
}

tasks.named("test") {
    dependsOn("generateAst")
}

tasks.register<JavaExec>("generateAst") {
    classpath = sourceSets.named("tool").get().runtimeClasspath
    mainClass.set("com.craftinginterpreters.tool.GenerateAst")
    args = listOf(project.file("app/src/main/java/com/craftinginterpreters/lox").getAbsolutePath())
}

sourceSets {
    create("tool") {
        java {
            srcDir("src/tool")
        }
    }
}

application {
    // Define the main class for the application.
    mainClass = "com.craftinginterpreters.lox.Lox"
}

At this point I’m pretty satisfied with the project set up. Now we’ve got:

  • Unit tests
  • Easy to build and run the project
  • Control the amount of log output
  • Code generation that runs automatically whenever we build

These quality of life improvements improved my experience working through the Java section of the book quite a bit. We love Gradle, we’re all gradle-pilled now.