Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
- name: Set up JDK 24
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
distribution: oracle
java-version: '24'
cache: 'maven'

- name: Build and verify
Expand All @@ -39,7 +39,7 @@ jobs:
for k in totals: totals[k]+=int(r.get(k,'0'))
except Exception:
pass
exp_tests=611
exp_tests=1354
exp_skipped=0
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
Expand Down
92 changes: 92 additions & 0 deletions json-java21-jtd-codegen/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.github.simbo1905.json</groupId>
<artifactId>parent</artifactId>
<version>0.1.9</version>
</parent>

<artifactId>java.util.json.jtd.codegen</artifactId>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The artifactId java.util.json.jtd.codegen is inconsistent with the module's directory name (json-java21-jtd-codegen) and the naming convention of other modules in this project (e.g., json-java21-jtd). For consistency and easier project navigation, consider renaming the artifactId to json-java21-jtd-codegen.

Suggested change
<artifactId>java.util.json.jtd.codegen</artifactId>
<artifactId>json-java21-jtd-codegen</artifactId>

<packaging>jar</packaging>
<name>java.util.json Java21 Backport JTD Codegen</name>
<url>https://simbo1905.github.io/java.util.json.Java21/</url>
<scm>
<connection>scm:git:https://github.com/simbo1905/java.util.json.Java21.git</connection>
<developerConnection>scm:git:git@github.com:simbo1905/java.util.json.Java21.git</developerConnection>
<url>https://github.com/simbo1905/java.util.json.Java21</url>
<tag>HEAD</tag>
</scm>
<description>Bytecode-generated JTD validators using the JDK 24+ ClassFile API.
Generates Java 21 compatible classfiles for hot-path validation.
Optional dependency: falls back to the interpreter path when absent.</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>24</maven.compiler.release>
</properties>

<dependencies>
<dependency>
<groupId>io.github.simbo1905.json</groupId>
<artifactId>java.util.json</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.github.simbo1905.json</groupId>
<artifactId>java.util.json.jtd</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>24</release>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Xdiags:verbose</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<release>24</release>
<doclint>none</doclint>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package json.java21.jtd.codegen;

import java.lang.constant.ClassDesc;
import java.lang.constant.ConstantDescs;
import java.lang.constant.MethodTypeDesc;

/// Shared class descriptors and method type descriptors for bytecode emission.
///
/// All fields are compile-time constants referencing the types the generated
/// classfiles interact with at runtime (JSON API, validation result types, JDK stdlib).
final class Descriptors {

private Descriptors() {}

// -- JDK types --
static final ClassDesc CD_Object = ConstantDescs.CD_Object;
static final ClassDesc CD_String = ConstantDescs.CD_String;
static final ClassDesc CD_Math = ClassDesc.of("java.lang.Math");
static final ClassDesc CD_CharSequence = ClassDesc.of("java.lang.CharSequence");
static final ClassDesc CD_OffsetDateTime = ClassDesc.of("java.time.OffsetDateTime");
static final ClassDesc CD_DateTimeFormatter = ClassDesc.of("java.time.format.DateTimeFormatter");
static final ClassDesc CD_Pattern = ClassDesc.of("java.util.regex.Pattern");
static final ClassDesc CD_Matcher = ClassDesc.of("java.util.regex.Matcher");

// -- Collections --
static final ClassDesc CD_ArrayList = ClassDesc.of("java.util.ArrayList");
static final ClassDesc CD_List = ClassDesc.of("java.util.List");
static final ClassDesc CD_Map = ClassDesc.of("java.util.Map");
static final ClassDesc CD_MapEntry = ClassDesc.of("java.util.Map$Entry");
static final ClassDesc CD_Set = ClassDesc.of("java.util.Set");
static final ClassDesc CD_Iterator = ClassDesc.of("java.util.Iterator");

// -- JSON API types --
static final ClassDesc CD_JsonValue = ClassDesc.of("jdk.sandbox.java.util.json.JsonValue");
static final ClassDesc CD_JsonObject = ClassDesc.of("jdk.sandbox.java.util.json.JsonObject");
static final ClassDesc CD_JsonArray = ClassDesc.of("jdk.sandbox.java.util.json.JsonArray");
static final ClassDesc CD_JsonString = ClassDesc.of("jdk.sandbox.java.util.json.JsonString");
static final ClassDesc CD_JsonNumber = ClassDesc.of("jdk.sandbox.java.util.json.JsonNumber");
static final ClassDesc CD_JsonBoolean = ClassDesc.of("jdk.sandbox.java.util.json.JsonBoolean");
static final ClassDesc CD_JsonNull = ClassDesc.of("jdk.sandbox.java.util.json.JsonNull");

// -- Validation result types --
static final ClassDesc CD_JtdValidationError = ClassDesc.of("json.java21.jtd.JtdValidationError");
static final ClassDesc CD_JtdValidationResult = ClassDesc.of("json.java21.jtd.JtdValidationResult");
static final ClassDesc CD_JtdValidator = ClassDesc.of("json.java21.jtd.JtdValidator");

// -- Common method type descriptors --
static final MethodTypeDesc MTD_String = MethodTypeDesc.of(CD_String);
static final MethodTypeDesc MTD_boolean = MethodTypeDesc.of(ConstantDescs.CD_boolean);
static final MethodTypeDesc MTD_double = MethodTypeDesc.of(ConstantDescs.CD_double);
static final MethodTypeDesc MTD_long = MethodTypeDesc.of(ConstantDescs.CD_long);
static final MethodTypeDesc MTD_int = MethodTypeDesc.of(ConstantDescs.CD_int);
static final MethodTypeDesc MTD_boolean_Object = MethodTypeDesc.of(ConstantDescs.CD_boolean, CD_Object);
static final MethodTypeDesc MTD_Object_Object = MethodTypeDesc.of(CD_Object, CD_Object);
static final MethodTypeDesc MTD_Object_int = MethodTypeDesc.of(CD_Object, ConstantDescs.CD_int);
static final MethodTypeDesc MTD_boolean_CharSequence = MethodTypeDesc.of(ConstantDescs.CD_boolean, CD_CharSequence);
static final MethodTypeDesc MTD_String_String = MethodTypeDesc.of(CD_String, CD_String);
static final MethodTypeDesc MTD_String_int = MethodTypeDesc.of(CD_String, ConstantDescs.CD_int);
static final MethodTypeDesc MTD_String_CharSeq_CharSeq = MethodTypeDesc.of(CD_String, CD_CharSequence, CD_CharSequence);
static final MethodTypeDesc MTD_Map = MethodTypeDesc.of(CD_Map);
static final MethodTypeDesc MTD_List = MethodTypeDesc.of(CD_List);
static final MethodTypeDesc MTD_Set = MethodTypeDesc.of(CD_Set);
static final MethodTypeDesc MTD_Iterator = MethodTypeDesc.of(CD_Iterator);
static final MethodTypeDesc MTD_Object = MethodTypeDesc.of(CD_Object);
static final MethodTypeDesc MTD_double_double = MethodTypeDesc.of(ConstantDescs.CD_double, ConstantDescs.CD_double);
static final MethodTypeDesc MTD_Pattern_String = MethodTypeDesc.of(CD_Pattern, CD_String);
static final MethodTypeDesc MTD_Matcher_CharSequence = MethodTypeDesc.of(CD_Matcher, CD_CharSequence);
static final MethodTypeDesc MTD_OffsetDateTime_CharSeq_DTF = MethodTypeDesc.of(CD_OffsetDateTime, CD_CharSequence, CD_DateTimeFormatter);
static final MethodTypeDesc MTD_void_String_String = MethodTypeDesc.of(ConstantDescs.CD_void, CD_String, CD_String);
static final MethodTypeDesc MTD_JtdValidationResult = MethodTypeDesc.of(CD_JtdValidationResult);
static final MethodTypeDesc MTD_JtdValidationResult_List = MethodTypeDesc.of(CD_JtdValidationResult, CD_List);
static final MethodTypeDesc MTD_JtdValidationResult_JsonValue = MethodTypeDesc.of(CD_JtdValidationResult, CD_JsonValue);
static final MethodTypeDesc MTD_void_String = MethodTypeDesc.of(ConstantDescs.CD_void, CD_String);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package json.java21.jtd.codegen;

import java.lang.classfile.CodeBuilder;
import java.lang.classfile.TypeKind;
import java.lang.constant.ConstantDescs;

import json.java21.jtd.JtdSchema;

import static json.java21.jtd.codegen.Descriptors.*;

/// Emits bytecode for JTD Discriminator schema (tagged union).
final class EmitDiscriminator {

private EmitDiscriminator() {}

static void emit(CodeBuilder cob, JtdSchema.DiscriminatorSchema d,
int instSlot, int errSlot,
String instPath, String schemaPath) {
var end = cob.newLabel();

// Step 1: must be object
var step1Fail = cob.newLabel();
cob.aload(instSlot);
cob.instanceOf(CD_JsonObject);
cob.ifeq(step1Fail);

cob.aload(instSlot);
cob.checkcast(CD_JsonObject);
cob.invokeinterface(CD_JsonObject, "members", MTD_Map);
int mapSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(mapSlot);

// Step 2: tag must exist
var step2Fail = cob.newLabel();
cob.aload(mapSlot);
cob.ldc(d.discriminator());
cob.invokeinterface(CD_Map, "containsKey", MTD_boolean_Object);
cob.ifeq(step2Fail);

cob.aload(mapSlot);
cob.ldc(d.discriminator());
cob.invokeinterface(CD_Map, "get", MTD_Object_Object);
cob.checkcast(CD_JsonValue);
int tagValSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(tagValSlot);

// Step 3: tag must be string
var step3Fail = cob.newLabel();
cob.aload(tagValSlot);
cob.instanceOf(CD_JsonString);
cob.ifeq(step3Fail);

cob.aload(tagValSlot);
cob.checkcast(CD_JsonString);
cob.invokeinterface(CD_JsonString, "string", MTD_String);
int tagStrSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(tagStrSlot);

// Step 4: dispatch to variants
for (final var entry : d.mapping().entrySet()) {
final var tagValue = entry.getKey();
final var variantSchema = entry.getValue();
var nextVariant = cob.newLabel();

cob.aload(tagStrSlot);
cob.ldc(tagValue);
cob.invokevirtual(CD_String, "equals", MTD_boolean_Object);
cob.ifeq(nextVariant);

if (variantSchema instanceof JtdSchema.PropertiesSchema props) {
EmitProperties.emit(cob, props, instSlot, errSlot, instPath,
schemaPath + "/mapping/" + tagValue, d.discriminator());
} else {
EmitNode.emit(cob, variantSchema, instSlot, errSlot, instPath,
schemaPath + "/mapping/" + tagValue);
}
cob.goto_(end);

cob.labelBinding(nextVariant);
}

// Step 5: tag not in mapping
EmitError.addError(cob, errSlot,
instPath + "/" + d.discriminator(), schemaPath + "/mapping");
cob.goto_(end);

// Error paths
cob.labelBinding(step1Fail);
EmitError.addError(cob, errSlot, instPath, schemaPath + "/discriminator");
cob.goto_(end);

cob.labelBinding(step2Fail);
EmitError.addError(cob, errSlot, instPath, schemaPath + "/discriminator");
cob.goto_(end);

cob.labelBinding(step3Fail);
EmitError.addError(cob, errSlot,
instPath + "/" + d.discriminator(), schemaPath + "/discriminator");

cob.labelBinding(end);
}

/// Dynamic-path variant: parent instancePath from local variable.
static void emitDynamic(CodeBuilder cob, JtdSchema.DiscriminatorSchema d,
int instSlot, int errSlot,
int pathSlot, String schemaPath) {
var end = cob.newLabel();

var step1Fail = cob.newLabel();
cob.aload(instSlot);
cob.instanceOf(CD_JsonObject);
cob.ifeq(step1Fail);

cob.aload(instSlot);
cob.checkcast(CD_JsonObject);
cob.invokeinterface(CD_JsonObject, "members", MTD_Map);
int mapSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(mapSlot);

var step2Fail = cob.newLabel();
cob.aload(mapSlot);
cob.ldc(d.discriminator());
cob.invokeinterface(CD_Map, "containsKey", MTD_boolean_Object);
cob.ifeq(step2Fail);

cob.aload(mapSlot);
cob.ldc(d.discriminator());
cob.invokeinterface(CD_Map, "get", MTD_Object_Object);
cob.checkcast(CD_JsonValue);
int tagValSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(tagValSlot);

var step3Fail = cob.newLabel();
cob.aload(tagValSlot);
cob.instanceOf(CD_JsonString);
cob.ifeq(step3Fail);

cob.aload(tagValSlot);
cob.checkcast(CD_JsonString);
cob.invokeinterface(CD_JsonString, "string", MTD_String);
int tagStrSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(tagStrSlot);

for (final var entry : d.mapping().entrySet()) {
final var tagValue = entry.getKey();
final var variantSchema = entry.getValue();
var nextVariant = cob.newLabel();

cob.aload(tagStrSlot);
cob.ldc(tagValue);
cob.invokevirtual(CD_String, "equals", MTD_boolean_Object);
cob.ifeq(nextVariant);

if (variantSchema instanceof JtdSchema.PropertiesSchema props) {
EmitProperties.emitDynamic(cob, props, instSlot, errSlot, pathSlot,
schemaPath + "/mapping/" + tagValue, d.discriminator());
} else {
EmitNode.emitDynamic(cob, variantSchema, instSlot, errSlot, pathSlot,
schemaPath + "/mapping/" + tagValue);
}
cob.goto_(end);

cob.labelBinding(nextVariant);
}

// tag not in mapping: error at pathSlot + "/" + discriminator
cob.aload(pathSlot);
cob.ldc("/" + d.discriminator());
cob.invokevirtual(CD_String, "concat", MTD_String_String);
int tagPathSlot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(tagPathSlot);
EmitError.addErrorDynamic(cob, errSlot, tagPathSlot, schemaPath + "/mapping");
cob.goto_(end);

cob.labelBinding(step1Fail);
EmitError.addErrorDynamic(cob, errSlot, pathSlot, schemaPath + "/discriminator");
cob.goto_(end);

cob.labelBinding(step2Fail);
EmitError.addErrorDynamic(cob, errSlot, pathSlot, schemaPath + "/discriminator");
cob.goto_(end);

cob.labelBinding(step3Fail);
cob.aload(pathSlot);
cob.ldc("/" + d.discriminator());
cob.invokevirtual(CD_String, "concat", MTD_String_String);
int tagPath2Slot = cob.allocateLocal(TypeKind.REFERENCE);
cob.astore(tagPath2Slot);
EmitError.addErrorDynamic(cob, errSlot, tagPath2Slot, schemaPath + "/discriminator");

cob.labelBinding(end);
}
}
Loading