Skip to content

Latest commit

 

History

History
334 lines (244 loc) · 15.2 KB

java.mdx

File metadata and controls

334 lines (244 loc) · 15.2 KB
title Java and Kotlin
description Read input with a reader spec from Java 27 with the native iris library, and from Kotlin with its extensions.
editUrl false
prev false
next false

import CheckedExample from '../../../components/docs/CheckedExample.astro';

The iris library is a native Java implementation of Iris. Its only dependency is the JSpecify annotations. It passes the same conformance suite as the Rust reference implementation, so it reads every input as the CLI does. The Java API reference lists every public class and method, and the Kotlin API reference lists the extensions and the Kotlin spec builder.

Add the library to a project

The library needs Java 27. It is not on Maven Central yet; publication is planned. Package versions and language versions are separate; the current library is 0.1.0 and implements Iris 0.1.

Until then, build it from a checkout. With Java 27 installed, run this in the java directory of an Iris checkout, which installs both modules in your local Maven repository:

A Gradle project then uses the coordinates dev.gecode:iris:0.1.0. Once the library is published, mavenLocal() can go:

A Maven project adds the same coordinates, and sets maven.compiler.release to 27:

The jar is the module dev.gecode.iris, so a modular application writes requires dev.gecode.iris;, and it also works on the class path.

Parse a reader spec and read input

The examples on this page are in java/iris-examples, and a test compiles each one, runs it and compares its output. They read files from the format library, so run them from the root of an Iris checkout.

Spec.parse parses and checks a reader spec. The declaration reads n, then a row of n integers. spec.read reads input from a String, a byte[] or a Path; the origin names, such as example.iris and input.txt, appear in error messages. A Spec can read any number of inputs.

Read files, instances and parameters

Spec.parse takes the bytes of a reader spec, so read the file yourself. spec.read and spec.catalog take a Path directly, and its text is the origin. They wrap an IOException in an UncheckedIOException.

spec.catalog lists the instances in an input as data. The instance parameter selects one instance during a read, by name or by its number, counting from 1. Params holds name and value strings, as the Specification defines: write Params.of("instance", "2"), not a number. Params.of also takes a Map, and params.with returns Params with one more value.

Access data

data.entries() returns the entries in the order the reader spec defines them, as records of a name and a Value. data.get(name) returns an entry's value, and data.find(name) returns it or null.

asLong, asDouble and asString return a single value. size, get and at walk an array, including arrays whose rows have different lengths; at takes one index per level, counted from 0. longs, ints and doubles return a one-dimensional row as a new primitive array on each call, and strings an unmodifiable list. ints throws ArithmeticException for an integer that does not fit an int.

Value is sealed, so a switch over it needs no default. Asking a Value for something it is not, or asking data.get for an unknown name, throws IllegalStateException or IndexOutOfBoundsException. These are mistakes in the calling code and never an IrisException.

dzn and JSON

data.toDzn() returns the MiniZinc data of the dzn appendix, and data.toJson() the canonical JSON document of the data model. Every line ends in a newline.

Handle errors

A language error is an IrisException, an unchecked exception that Spec.parse, spec.read and spec.catalog throw. getMessage() is the diagnostic that Specification chapter 11 describes.

code() returns the ErrorCode of the stable error code, and its code() the spelling. Compare codes, not messages. origin(), line() and column() give the location in the input if the error has one, and otherwise in the reader spec; each can be null. path() gives the indices of a failed check's witness.

error() returns the IrisError with all the details: message(), inputLocation(), specLocation(), context() and recordPath(), and toJson() for the canonical error document. It is sealed, with one record for each of SpecError, ReadError and CheckError.

The language's fixed work budgets bound every read. A read that exhausts one fails with the code E-BUDGET, and the library has no budget settings.

Kotlin

The module dev.gecode:iris-kotlin adds extensions to the Java API. It depends on dev.gecode:iris and the Kotlin standard library, and nothing else. It is not on Maven Central yet either; the command above installs it too.

In Maven:

The module still needs Java 27 to run, but Kotlin has no JVM 27 target yet, so its classes target JVM 26. A Kotlin project does the same. On Java 27, Gradle would otherwise reject the difference between the Java and Kotlin targets:

The Java API works from Kotlin as it is. data["n"] and value[0] call the Java get methods as operators.

The Java API is @NullMarked, so Kotlin sees non-null types, and a result marked @Nullable, such as that of data.find, as nullable. The extensions add indexing by several levels at once, arrays and sequences:

value[i, j] selects what value.at(i, j) does. toIntArray, toLongArray and toDoubleArray return a new array, and toIntArray throws ArithmeticException as ints does. asSequence walks the elements of an array or the entries of Data, leaves the single values at any depth in row order, and toParams makes Params from a map. Misuse throws what the Java API throws.

IrisError is sealed in Kotlin as well, so a when over it needs no else:

Build a reader spec

dev.gecode.iris.build writes a reader spec from Java. A handle names an entry with its value type and depth: int0("n_jobs") is an IntField<Long> and int2("machine") an IntField<List<long[]>>. Statements use the handles, and after a read data.get(handle) returns the entry as the handle's type. A builder adds no features of its own: spec(...) prints Iris text and parses it as Spec.parse does, so the language's checks decide what is valid, and builder(...).source() returns the text. The spec builder reference lists its classes.

The builders below are in java/iris-examples, one for each format in the format library, in Java and in Kotlin, each with a program that reads a case. A test reads every case of each format through both builders and compares the data or the error with the case's expected results, and the example tests run each program and compare its output with the output shown here.

The job shop builder

This builder writes the job shop reader spec, and its program reads an instance selected by name, then the same entries into a record, then a file whose check fails:

The handles are constants, and spec is a constant built from them. With import static dev.gecode.iris.build.Iris.*, the statements read like the spec: blocks take a lambda, and the statements it makes go into the block, so instances, sections, arm and repeat nest as their Iris blocks do. capture(nJobs, atLeast(1)) puts a field into a pattern, and range(0).below(nMachines) is Iris 0..<n_machines. Methods such as le, minus and div build expressions, and Gecode.intMax is Gecode's largest integer.

recordName("job", "jobs") names each record of a repeat and the list of records, as in repeat n_jobs as jobs with each job, for error messages and comprehensions: job.get(machine) looks up a field of each record, and sum(job, j -> ...) writes a comprehension over them.

Typed data and records

data.get returns what the handle's type says: a Long for nJobs, a nullable String for name, which only some files give, and a List<long[]> for machine and duration, with a long[] for each job. A lookup checks the entry's value type, depth and presence, and throws IllegalStateException if they differ from the handle's.

spec.reader(Shop.class, MethodHandles.lookup()) reads the same entries into a record. Each component names its entry, or takes the name from @Entry, and its type decides the value as a handle's does; a long component reads a required integer. reader checks the components against the spec's entries, and throws IllegalArgumentException for one that names no entry or does not fit its entry, so a read can fail only as spec.read does.

Errors name your code

A builder records where each statement was made. When a check fails while reading, the error has the input location, and error.callSite() names the line of the check in BuildJobshop.java, in your code rather than in the library. A BuildException from spec(...) has the same call site. A statement made in a helper method records the line in the helper.

A helper used twice

A method that makes statements is a reusable piece of a reader spec. The nonogram builder reads the clues of the rows and of the columns with one helper, clueLines, and builds two pairs of checks with two expression helpers:

A builder rejects a second definition of a name, so a helper called twice by mistake fails spec(...) instead of quietly replacing an entry. That is why clueLines takes its handle from its caller. The TSPLIB builder below uses the same idea for its two coordinate sections.

The other formats

defineType gives a named type, used here for both ends of a time window:

A TSPLIB file gives either a distance matrix or coordinates. set gives an optional entry a default while keeping its handle's type, ifThen and otherwise take the checks that apply to each kind of file, and oneOf is an ordinary method that builds an expression:

In Kotlin

dev.gecode.iris.kotlin.build gives the builder Kotlin's syntax: blocks are trailing lambdas, infix functions such as le and operators such as - and / build expressions, and a property delegate such as val nJobs by capture(atLeast(1), name = "n_jobs") defines an entry and its handle at once. irisSpec returns the spec with what its lambda returns, here a class of handles, and the pair destructures as val (spec, shop) = jobshop. data[shop.machine] calls the Java get as an operator, and the Java types become Kotlin's: List<LongArray> for machine and String? for name. The Kotlin spec builder reference lists irisSpec, the statements of a scope and the capture delegates.

Typed handles for a text spec

A reader spec written as an .iris file has no builder, but iris gen --lang java SPEC prints a class with a typed handle for each of its entries, named after the spec's file:

Save it in your package, such as Jobshop.java with a package line after the first comment, and read data with data.get(Jobshop.machine) as with a builder's handles. An entry whose value type or depth differs between the places that define it gets a DynField, which returns a Value, and an entry deeper than three levels returns a Value too. The class qualifies the types of java.lang, such as java.lang.Long, so that a class named Long or String in your package cannot take their place. iris gen --lang kotlin SPEC prints an object of the same handles for Kotlin, which imports the standard library's types for the same reason:

Handles at compile time

The annotation processor dev.gecode:iris-processor writes the same handles while javac compiles, so they follow the .iris file without a step of their own. It is needed only to compile:

-Airis.root names the directory that relative paths start from; without it, a path starts at the directory of the annotated source file. Declaring the specs as inputs, here a directory specs, recompiles when one changes. The processor leaves the annotation unclaimed, so a build with -Xlint:all -Werror adds -Xlint:-processing. In Maven, the processor goes in the compiler plugin's annotationProcessorPaths.

On a top-level type annotated with @IrisSpec, the processor checks the spec and writes two classes beside the type: JobshopHandles, with the parsed spec as spec, the handles that iris gen --lang java prints, and a record reader as reader, and JobshopData, the record that reader reads. An invalid spec fails the compilation with the diagnostic at the annotation. A spec whose entries take more than 254 parameter slots in a record, where a long or double component takes two, gets the handles but no record or reader, since a constructor takes at most 254; the processor notes it. The generated classes are laid out as google-java-format lays them out.

The processor's reference lists the annotation and its options. The processor runs in javac, so Kotlin code uses the handles of an annotated Java type, or the object that iris gen --lang kotlin prints.