| 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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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 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.
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:
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.
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:
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.