STRUMENTA LANGUAGE ENGINES · JAVA

A Java engine for pipelines where Java is not the only language.

Modeled on the Java Language Specification, SE 20. All final syntax through Java 17 — records, sealed types, switch expressions, text blocks, modules — with an AST that is specified rather than inferred, a code generator, and symbol resolution.

Java already has an excellent free parser. If Java is the only language you need to read, use javaparser and we will tell you so. This engine exists for the case where the Java is one end of a COBOL, RPG or SAS migration.

The engine at a glance
SPECIFICATIONJLS SE 20
SYNTAX CEILINGJava 17 final syntax
AST278 specified node definitions
TESTS331 across four modules
OUTPUTAST · JSON · XML · LionWeb
WRITES JAVAYes — printer and placeholders

The grammar is the easy half

JavaParser.g4 · JavaLexer.g4 · from antlr/grammars-v4 · BSD

Java has a real, precise, freely published grammar in Chapter 19 of the specification. That is exactly why good free parsers exist, and we are not going to pretend otherwise. The difficulty in Java is not writing the grammar — it is everything the grammar deliberately leaves undecided.

Start with the classic: < is ambiguous. a < b is a comparison, List<String> is a type, and a<b,c>d could be either. A nested generic ending in >> collides with the shift operator, which is why a real Java lexer has to split that token back apart.

Then the contextual keywords. var, yield, record, sealed, permits, module, open and requires are all still legal identifiers. The grammar we ship carries an explicit identifier rule that lists them, which is why a program with a method called record and a parameter called var still parses.

Then the part no grammar can reach: a.b.c.d may be package, package, class, field — or class, field, field, field — or variable, field, field, field. The specification spends a whole chapter on the reclassification of contextually ambiguous names. This is where a bare grammar stops and where the semantic module starts.

And text blocks, which look like a substring and are not. The closing delimiter’s indentation sets the common prefix that gets stripped, \s preserves one trailing space, and a trailing backslash suppresses the newline. Computing the value is an algorithm; there are four dedicated tests and four fixture files behind it.

Input — Java 17

Shape.java

public sealed interface Shape
        permits Circle, Square, Rect {}
record Circle(double r) implements Shape {}

final class Report {
  static double area(Shape s) {
    return switch (s) {
      case Circle c -> Math.PI * c.r() * c.r();
      case Rect   r -> {
        double a = r.w() * r.h();
        yield a;
      }
    };
  }
  static int record(int var) { return var; }
}
The last line is the point. record is a method name and var is a parameter name, and both are legal — four lines after record and sealed were used as declarations.

Output — parse tree, by grammar rule

intermediate rules elided

compilationUnit
├── typeDeclaration
│   └── interfaceDeclaration Shape, SEALED
│       └── typeList permitted = Circle, Square, Rect
├── typeDeclaration
│   └── recordDeclaration Circle
│       └── recordHeader
│           └── recordComponent double r
└── typeDeclaration
    └── classDeclaration Report, FINAL
        ├── methodDeclaration area
        │   └── switchExpression 3 labeled rules
        │       └── yieldStatement line 10
        └── methodDeclaration identifier = record
            └── formalParameter identifier = var
These are rule names from the grammar we ship, not a drawing of one. The AST you program against is a separate, specified artifact derived from this tree — see the next section — and every node carries a source position.

Which Java, exactly

FINAL SYNTAX ONLY NO PREVIEW FEATURES

The version question is the first one a technical buyer asks and the one most vendor pages avoid. Here is the whole answer, including the parts that are missing.

Feature by feature, checked against the grammar we ship. The last two rows are as important as the others.
Feature Since Status
Generics, lambdas, annotations, enums, inner and anonymous classes Java 5–8 Supported
try-with-resources Java 7 Supported
Modules — module-info.java, requires, exports Java 9 Supported
var for local variables Java 10 Supported
Switch expressions with yield Java 14 Supported
Text blocks, including the incidental-whitespace rules Java 15 Supported — four tests, four fixtures
Records Java 16 Supported
Pattern matching for instanceof Java 16 Supported
Sealed classes and interfaces, permits, non-sealed Java 17 Supported
Java 18, 19 and 20 Nothing to add — no new final syntax shipped in those releases
Record patterns and pattern matching for switch Java 21 Not in the grammar — ask us before you plan around it
Preview features, under --enable-preview any Deliberately not supported — there is no switch to turn them on

The preview row is a policy, not an omission. Preview syntax changes between releases and sometimes disappears; a parser that guesses at it produces trees that stop being true. We track final syntax. If your codebase depends on a preview feature, tell us which one and we will give you a real answer rather than a badge.

Four places a naive Java parser gets it wrong

Contextual keywords

Ten words are keywords in one position and identifiers in another. The grammar lists them explicitly as identifiers, which is the only reason real codebases — full of variables called module and methods called yield — still parse after a language upgrade.

int record = 1, sealed = 2, var = 3;
var permits = List.of(record, sealed);

Text blocks are not substrings

The closing delimiter’s indentation decides how much leading whitespace is stripped from every line, \s is a text-block-only escape that preserves one space, and a trailing backslash joins lines. The content arrives as a node with a position, so an embedded query can be handed to another engine.

String q = """
    SELECT id FROM emp \
    WHERE dept = ?\s
    """;

Annotations on types, not only declarations

Since Java 8 an annotation may sit anywhere a type does — inside a generic argument, between the element type and a bracket pair. These produce some of the least pleasant productions in the specification, and they are the ones nullness checkers actually use.

List<@NonNull String> names;
@Nullable String[] @NonNull [] grid;

Lambda, cast, or parentheses

(a, b) -> is a lambda, (Foo) x is a cast, (Foo & Bar) x is an intersection cast and (a) is just a parenthesized expression. Telling them apart needs arbitrary lookahead — there is no bounded prefix that decides it.

Runnable r = (a, b) -> log(a, b);
Foo f = (Comparable & Serializable) x;

The AST is specified, not inferred

Most parsers hand you whatever shape the grammar happened to produce. This one has a written model: 248 node definitions in the AST specification plus 30 more for statements, each carrying a doc comment citing the clause of the Java specification it comes from.

Input — the file the semantic module exists for

Report.java · Java 17 final syntax throughout

package com.example.shapes;

import java.util.List;

public sealed interface Shape permits Circle, Square, Rect {}

record Circle(double r)    implements Shape {}
record Square(double side) implements Shape {}
record Rect(double w, double h) implements Shape {}

final class Report {

  /** The switch is an expression, so it must be exhaustive — which is
   *  only provable because Shape is sealed. */
  static double area(Shape s) {
    return switch (s) {                // exhaustive via permits
      case Circle c -> Math.PI * c.r() * c.r();
      case Square q -> q.side() * q.side();
      case Rect   r -> {
        yield r.w() * r.h();           // a statement here, a name elsewhere
      }
    };
  }

  static String render(List<? extends Shape> shapes) {
    var total = shapes.stream().mapToDouble(Report::area).sum();
    return """
           { "count": %d, "total": %.3f }
           """.formatted(shapes.size(), total);
  }

  static int record(int var) { return var; }   // still legal
}
Whether this file even compiles depends on the permits clause on a type declared elsewhere in the same compilation. Syntax alone cannot tell you; that is what the symbol-resolution module is for — 103 tests covering lambda and type-parameter resolution, enum resolution, reflection-backed resolution of JDK types and LionWeb-backed resolution models.
WHAT THIS ENGINE IS ACTUALLY FOR

Java is usually the target, not the source.

A COBOL-to-Java or RPG-to-Java project does not need something that reads Java. It needs something that writes it — and can prove the file it wrote parses back to the tree it started from.

01

Read the legacy

What is in the estate?

The COBOL, RPG or SAS engine parses the source. Because every engine is built on Starlasu, the tree it hands you has the same shape and the same traversal model as the Java one.

02

Map into one model

What should it become?

Your transformation walks the legacy tree and builds Java nodes. The Java AST is a specified artifact — 278 node definitions with doc comments citing the language specification — so you build against a document, not a guess.

03

Fill the holes

What comes from a template?

The placeholder system lets you build a Java tree with gaps and fill them later, which is how a transpiler keeps hand-written scaffolding and generated logic in one model instead of two text files.

04

Print, comments intact

Is it Java again?

The printer emits Java source. Comments are on the hidden channel rather than discarded, and there is a dedicated printing path for them, so a mechanical sweep over ten thousand files does not strip the explanation of why the code is odd.

Golden files, not vibes. The repository carries paired source and serialized-AST examples: parse the source, compare against the committed tree, print it back, compare again. That is the check that catches a grammar upgrade quietly changing the shape of something.

The extensive check runs against real codebases. Not “extensively tested” — named ones, cloned in CI on both JDK 17 and JDK 21: spring-boot, react-native, elasticsearch, RxJava and arthas. If a change breaks Elasticsearch, the build says so.

Symbol resolution is real. There is a semantic module with an enricher, an importer, an AST loader and file-level contexts, backed by 103 tests. It resolves lambdas, type parameters and enums, reaches JDK types through reflection, and can be driven from a LionWeb-backed model. If you have read our README and seen a line saying otherwise, that line is out of date and the module contradicts it.

What it does not do: it does not read your pom.xml or your Gradle build. Assembling a codebase from a build system is the caller’s job, and we would rather say that than let you discover it.

331
AUTOMATED TESTS ACROSS FOUR MODULES
103
OF THEM FOR SYMBOL RESOLUTION
72
FOR PRINTING JAVA BACK OUT
5
OPEN-SOURCE CODEBASES IN THE CI CHECK

What you receive

A JVM library and a command-line tool, the documents that describe the model, and a license with support in it. The engine runs inside your own network, on your own machines.

01 The engine

A library and a command-line tool

Kotlin on the JVM, built with ANTLR on Kolasu — the JVM implementation of Starlasu. Use it in process from Java or Kotlin, or run the command-line tool over a directory and collect the AST serialized as JSON or XML. Java 17 or later; the build is verified on JDK 17 and 21. A license file is registered at start-up; licenses are refreshed from a license service, so tell us early if the engine has to run air-gapped.

library · CLI · JDK 17 and 21
02 The model

A specified AST, positions, comments and LionWeb

A typed AST rooted at a compilation unit, defined in a written specification of 248 node definitions plus 30 for statements, each citing the language specification clause behind it. Positions on every node, comments retained rather than discarded, and LionWeb export so the model travels to tooling we did not write. Bindings from Java, Kotlin, Python, TypeScript and C#.

JSON · XML · LionWeb · five bindings
03 The write path

A printer, placeholders, and a pipeline stage

A code-generation module that emits Java from a modified or newly built AST, with 72 printing tests and a dedicated path for comments; a placeholder system for template-driven generation; and a drop-in Starlasu pipeline component, so the Java stage of a migration is a configuration rather than an integration.

printer · placeholders · pipeline stage
04 The license

Standard, Distribution or Service — support included

Standard for use inside your own organization, Distribution if the engine ships inside a product you sell, Service if it runs behind a service you operate. Support is part of the license, not an upsell. The guarantee runs a year and is extendable, or can be taken pay-as-you-go.

three tiers · support included
What the model is good for

Every declaration, branch and loop as a typed, positioned node — which is what a metrics traversal is written against.

Counting classes, module size or decision points is a visitor you write once against a stable model. We do not ship a metrics dashboard or a code-quality product, and we would rather tell you that than have you buy one that is not in the box.

WHY THIS ONE

javaparser is free, and good. Here is what it cannot do.

Not a superiority claim. javaparser tracks the specification closely and costs nothing. It also gives you Java, and only Java, in a shape nothing else in your pipeline speaks.

Every Strumenta engine is built on Starlasu, so every one produces an AST in the same shape, with the same APIs and the same traversal model. The visitor you wrote for COBOL walks the Java tree. Five unrelated open-source parsers means five models, five idioms and five sets of edge cases to reconcile before you have written a line of analysis.

Every engine supports LionWeb, so the models interchange with LionWeb-compliant tooling rather than being trapped in one process — and every engine is reachable through bindings from Java, Kotlin, Python, TypeScript and C#. The host application picks the language.

So: you license the Java engine when Java is one language in a multi-language pipeline. A COBOL-to-Java or RPG-to-Java migration. A cross-language dependency analysis. A polyglot inventory. You do not license it to parse Java on its own, and a free Java parser does not compose with your COBOL parser.

Engines in the same pipeline

one model, one traversal

COBOL engine →
The source side of a COBOL-to-Java move.

RPG & DDS engine →
The IBM i estate, with the DDS beside it.

Kotlin engine →
The other half of a modern JVM codebase.

The whole catalog →
15+ engines, one AST framework underneath.

When to buy this, and when not to

Two columns, and we mean both of them. The left one is a shorter list than most vendors would print.

License it when…

  • Java is the target of a migration from COBOL, RPG, EGL or SAS, and you need to generate it rather than only read it.
  • Java is one language among several and you need a single dependency map across all of them.
  • You need the analysis result to outlive the process — a LionWeb model repository, another team’s tooling, another language.
  • Your tool is written in Python, TypeScript or C# and you would rather not host a JVM parser API yourself.
  • You are doing a large mechanical refactor where losing comments is unacceptable.

Do not license it when…

  • You need to parse Java, alone, inside a JVM tool. javaparser is free, actively maintained and closely tracks the specification. Use it.
  • You need Java 21 record patterns or switch patterns today. They are not in the grammar.
  • You need preview features. We deliberately do not implement them.
  • You need bytecode. This is a source engine.
  • You want the parser to resolve your build. It reads files; Maven and Gradle are your side of the line.

The method behind the work has a name: read the Chisel Method for how we make parser work estimable, or Transpilers for what the generation side of a migration actually looks like.

What to discuss before you license

Six things we would rather tell you now than have you find in week three.

  • The syntax ceiling is Java 17. The grammar descends from the public antlr/grammars-v4 Java grammar, upgraded to Java 17 in 2022, and it is credited as such. Java 18, 19 and 20 added no new final syntax, which is why the specification we document against is SE 20. Java 21 record patterns and pattern matching for switch are not there.
  • No preview features, by policy. Not a gap to be closed on request — a deliberate position. Preview syntax changes and occasionally disappears, and a tree built from it stops being true.
  • String and text-block contents are not parsed. An embedded SQL query is a positioned literal node, which is the right answer — hand it to the matching Strumenta SQL engine rather than expecting this one to guess the dialect.
  • No build-system integration. The engine parses files. It does not read pom.xml, build.gradle, module paths or classpaths, and it does not decide which files constitute your codebase.
  • It runs on a JVM. Java 17 or later, in your environment. If your toolchain is Python, TypeScript or C#, you use the bindings or the serialized tree — which works well, and is worth designing for deliberately rather than discovering late.
  • The license is a file, refreshed from a service. The engine registers a license file at start-up and the license expires on the order of a month. In a connected environment that is invisible; in an air-gapped one it needs designing for, so raise it in the first call rather than the last.

What we will ask you

  1. Is Java the only language in the pipeline? If it is, we will point you at javaparser.
  2. Which other languages are in scope — COBOL, RPG, SAS, SQL, EGL?
  3. Is Java the source you are analyzing, or the target you are generating?
  4. What Java version is the codebase, and does anything depend on Java 21 syntax?
  5. Do comments and formatting have to survive the transformation?
  6. Do you need symbol resolution and call graphs, or is a syntax tree enough?
  7. What language is your tool written in?
  8. Do the models have to leave the process — a LionWeb repository, another team’s tooling?

Straight answers

Why would we pay for a Java parser when javaparser is free?

Usually you would not, and we will say so. javaparser is free, good and current. The reason to license this one is composition: it is built on Starlasu, so its tree has the same shape as the COBOL, RPG, SAS and SQL trees, and one visitor walks all of them. A free Java parser does not compose with your COBOL parser. If Java is the only language in the pipeline, use javaparser.

Which Java version does it parse?

All final language syntax through Java 17 — records, sealed types and permits, switch expressions with yield, text blocks, modules, pattern matching for instanceof, var. Java 18, 19 and 20 introduced no further final syntax, and the specification we document against is SE 20. Java 21 record patterns and pattern matching for switch are not in the grammar yet, and preview features are deliberately not implemented at all.

Can it write Java, or only read it?

Both. There is a code-generation module that prints Java from an AST, with 72 printing tests and a dedicated path for comments, plus a placeholder system for template-driven generation. That is the half a COBOL-to-Java or RPG-to-Java project actually needs, and it is why this engine exists.

Does it do symbol resolution?

Yes. There is a semantic module with an enricher, an importer, an AST loader and file-level contexts, covered by 103 tests: lambda resolution, type-parameter resolution, enum resolution, reflection-backed resolution of JDK types, and LionWeb-backed resolution models. It does not resolve your build, though — it does not read Maven or Gradle files to work out what your codebase is.

What has it been run against?

331 automated tests across the parsing, semantic, generation and pipeline modules, plus golden-file round trips that compare a parsed tree against a committed expected AST. On top of that, a CI check clones and parses five real open-source codebases on both JDK 17 and JDK 21: spring-boot, react-native, elasticsearch, RxJava and arthas.

Tell us the other language that is the interesting half

If Java is the only language, we will send you elsewhere.

Tell us what else is in the estate — COBOL, RPG, SAS, SQL — and which direction the Java runs, source or target. We will come back with what one model across all of it would actually give you, and whether it is worth a license. If it is not, that is a short call and it costs you nothing. If you would rather see the model before the call, the Strumenta Playground parses Java in your browser — pick Java from the language list.

Scroll to Top