Skip to content

Commit c1805a9

Browse files
committed
JTD bytecode codegen via ClassFile API + RFC 8927 conformance
Adds json-java21-jtd-codegen module (JDK 24+, --release 24) that compiles JTD schemas into bytecode validators targeting Java 21. Generated classfiles use the JDK 24 ClassFile API (JEP 484) and are loaded at runtime via MethodHandles.Lookup.defineClass(). Runtime API (json-java21-jtd, Java 21): - JtdValidator functional interface: JsonValue -> JtdValidationResult - JtdValidator.compile(schema) -- interpreter path, always available - JtdValidator.compileGenerated(schema) -- codegen path via reflection - JtdValidationResult record with RFC 8927 (instancePath, schemaPath) - InterpreterValidator wraps the existing stack machine Codegen module (json-java21-jtd-codegen, JDK 24+): - Modular emitter architecture: EmitNode dispatches to per-form emitters (EmitType, EmitEnum, EmitElements, EmitProperties, EmitValues, EmitDiscriminator) - Lazy instance path construction: deferred concat only on error - Average 9.4x faster than interpreter on valid documents RFC 8927 conformance: - Schema path corrections per official validation suite: Elements/Values/Properties/Discriminator type guards, Properties conditional guard (/properties vs /optionalProperties), Ref paths use /definitions/<name>/... - 316/316 official json-typedef-spec validation.json cases pass (interpreter); 314/316 codegen (2 recursive schemas skipped) Verification: - json-java21-jtd: 452 tests (136 unit + 316 spec conformance) - json-java21-jtd-codegen: 398 tests (82 cross-validation + 316 spec) - Total: 850 tests, all passing
1 parent d943174 commit c1805a9

39 files changed

+17606
-11
lines changed

json-java21-jtd-codegen/pom.xml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5+
http://maven.apache.org/xsd/maven-4.0.0.xsd">
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<parent>
9+
<groupId>io.github.simbo1905.json</groupId>
10+
<artifactId>parent</artifactId>
11+
<version>0.1.9</version>
12+
</parent>
13+
14+
<artifactId>java.util.json.jtd.codegen</artifactId>
15+
<packaging>jar</packaging>
16+
<name>java.util.json Java21 Backport JTD Codegen</name>
17+
<url>https://simbo1905.github.io/java.util.json.Java21/</url>
18+
<scm>
19+
<connection>scm:git:https://github.com/simbo1905/java.util.json.Java21.git</connection>
20+
<developerConnection>scm:git:git@github.com:simbo1905/java.util.json.Java21.git</developerConnection>
21+
<url>https://github.com/simbo1905/java.util.json.Java21</url>
22+
<tag>HEAD</tag>
23+
</scm>
24+
<description>Bytecode-generated JTD validators using the JDK 24+ ClassFile API.
25+
Generates Java 21 compatible classfiles for hot-path validation.
26+
Optional dependency: falls back to the interpreter path when absent.</description>
27+
28+
<properties>
29+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
30+
<maven.compiler.release>24</maven.compiler.release>
31+
</properties>
32+
33+
<dependencies>
34+
<dependency>
35+
<groupId>io.github.simbo1905.json</groupId>
36+
<artifactId>java.util.json</artifactId>
37+
<version>${project.version}</version>
38+
</dependency>
39+
<dependency>
40+
<groupId>io.github.simbo1905.json</groupId>
41+
<artifactId>java.util.json.jtd</artifactId>
42+
<version>${project.version}</version>
43+
</dependency>
44+
45+
<!-- Test dependencies -->
46+
<dependency>
47+
<groupId>org.junit.jupiter</groupId>
48+
<artifactId>junit-jupiter-api</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>org.junit.jupiter</groupId>
53+
<artifactId>junit-jupiter-engine</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.junit.jupiter</groupId>
58+
<artifactId>junit-jupiter-params</artifactId>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>org.assertj</groupId>
63+
<artifactId>assertj-core</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
</dependencies>
67+
68+
<build>
69+
<plugins>
70+
<plugin>
71+
<groupId>org.apache.maven.plugins</groupId>
72+
<artifactId>maven-compiler-plugin</artifactId>
73+
<version>3.13.0</version>
74+
<configuration>
75+
<release>24</release>
76+
<compilerArgs>
77+
<arg>-Xlint:all</arg>
78+
<arg>-Xdiags:verbose</arg>
79+
</compilerArgs>
80+
</configuration>
81+
</plugin>
82+
<plugin>
83+
<groupId>org.apache.maven.plugins</groupId>
84+
<artifactId>maven-javadoc-plugin</artifactId>
85+
<configuration>
86+
<release>24</release>
87+
<doclint>none</doclint>
88+
</configuration>
89+
</plugin>
90+
</plugins>
91+
</build>
92+
</project>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package json.java21.jtd.codegen;
2+
3+
import java.lang.constant.ClassDesc;
4+
import java.lang.constant.ConstantDescs;
5+
import java.lang.constant.MethodTypeDesc;
6+
7+
/// Shared class descriptors and method type descriptors for bytecode emission.
8+
///
9+
/// All fields are compile-time constants referencing the types the generated
10+
/// classfiles interact with at runtime (JSON API, validation result types, JDK stdlib).
11+
final class Descriptors {
12+
13+
private Descriptors() {}
14+
15+
// -- JDK types --
16+
static final ClassDesc CD_Object = ConstantDescs.CD_Object;
17+
static final ClassDesc CD_String = ConstantDescs.CD_String;
18+
static final ClassDesc CD_Math = ClassDesc.of("java.lang.Math");
19+
static final ClassDesc CD_CharSequence = ClassDesc.of("java.lang.CharSequence");
20+
static final ClassDesc CD_OffsetDateTime = ClassDesc.of("java.time.OffsetDateTime");
21+
static final ClassDesc CD_DateTimeFormatter = ClassDesc.of("java.time.format.DateTimeFormatter");
22+
static final ClassDesc CD_Pattern = ClassDesc.of("java.util.regex.Pattern");
23+
static final ClassDesc CD_Matcher = ClassDesc.of("java.util.regex.Matcher");
24+
25+
// -- Collections --
26+
static final ClassDesc CD_ArrayList = ClassDesc.of("java.util.ArrayList");
27+
static final ClassDesc CD_List = ClassDesc.of("java.util.List");
28+
static final ClassDesc CD_Map = ClassDesc.of("java.util.Map");
29+
static final ClassDesc CD_MapEntry = ClassDesc.of("java.util.Map$Entry");
30+
static final ClassDesc CD_Set = ClassDesc.of("java.util.Set");
31+
static final ClassDesc CD_Iterator = ClassDesc.of("java.util.Iterator");
32+
33+
// -- JSON API types --
34+
static final ClassDesc CD_JsonValue = ClassDesc.of("jdk.sandbox.java.util.json.JsonValue");
35+
static final ClassDesc CD_JsonObject = ClassDesc.of("jdk.sandbox.java.util.json.JsonObject");
36+
static final ClassDesc CD_JsonArray = ClassDesc.of("jdk.sandbox.java.util.json.JsonArray");
37+
static final ClassDesc CD_JsonString = ClassDesc.of("jdk.sandbox.java.util.json.JsonString");
38+
static final ClassDesc CD_JsonNumber = ClassDesc.of("jdk.sandbox.java.util.json.JsonNumber");
39+
static final ClassDesc CD_JsonBoolean = ClassDesc.of("jdk.sandbox.java.util.json.JsonBoolean");
40+
static final ClassDesc CD_JsonNull = ClassDesc.of("jdk.sandbox.java.util.json.JsonNull");
41+
42+
// -- Validation result types --
43+
static final ClassDesc CD_JtdValidationError = ClassDesc.of("json.java21.jtd.JtdValidationError");
44+
static final ClassDesc CD_JtdValidationResult = ClassDesc.of("json.java21.jtd.JtdValidationResult");
45+
static final ClassDesc CD_JtdValidator = ClassDesc.of("json.java21.jtd.JtdValidator");
46+
47+
// -- Common method type descriptors --
48+
static final MethodTypeDesc MTD_String = MethodTypeDesc.of(CD_String);
49+
static final MethodTypeDesc MTD_boolean = MethodTypeDesc.of(ConstantDescs.CD_boolean);
50+
static final MethodTypeDesc MTD_double = MethodTypeDesc.of(ConstantDescs.CD_double);
51+
static final MethodTypeDesc MTD_long = MethodTypeDesc.of(ConstantDescs.CD_long);
52+
static final MethodTypeDesc MTD_int = MethodTypeDesc.of(ConstantDescs.CD_int);
53+
static final MethodTypeDesc MTD_boolean_Object = MethodTypeDesc.of(ConstantDescs.CD_boolean, CD_Object);
54+
static final MethodTypeDesc MTD_Object_Object = MethodTypeDesc.of(CD_Object, CD_Object);
55+
static final MethodTypeDesc MTD_Object_int = MethodTypeDesc.of(CD_Object, ConstantDescs.CD_int);
56+
static final MethodTypeDesc MTD_boolean_CharSequence = MethodTypeDesc.of(ConstantDescs.CD_boolean, CD_CharSequence);
57+
static final MethodTypeDesc MTD_String_String = MethodTypeDesc.of(CD_String, CD_String);
58+
static final MethodTypeDesc MTD_String_int = MethodTypeDesc.of(CD_String, ConstantDescs.CD_int);
59+
static final MethodTypeDesc MTD_String_CharSeq_CharSeq = MethodTypeDesc.of(CD_String, CD_CharSequence, CD_CharSequence);
60+
static final MethodTypeDesc MTD_Map = MethodTypeDesc.of(CD_Map);
61+
static final MethodTypeDesc MTD_List = MethodTypeDesc.of(CD_List);
62+
static final MethodTypeDesc MTD_Set = MethodTypeDesc.of(CD_Set);
63+
static final MethodTypeDesc MTD_Iterator = MethodTypeDesc.of(CD_Iterator);
64+
static final MethodTypeDesc MTD_Object = MethodTypeDesc.of(CD_Object);
65+
static final MethodTypeDesc MTD_double_double = MethodTypeDesc.of(ConstantDescs.CD_double, ConstantDescs.CD_double);
66+
static final MethodTypeDesc MTD_Pattern_String = MethodTypeDesc.of(CD_Pattern, CD_String);
67+
static final MethodTypeDesc MTD_Matcher_CharSequence = MethodTypeDesc.of(CD_Matcher, CD_CharSequence);
68+
static final MethodTypeDesc MTD_OffsetDateTime_CharSeq_DTF = MethodTypeDesc.of(CD_OffsetDateTime, CD_CharSequence, CD_DateTimeFormatter);
69+
static final MethodTypeDesc MTD_void_String_String = MethodTypeDesc.of(ConstantDescs.CD_void, CD_String, CD_String);
70+
static final MethodTypeDesc MTD_JtdValidationResult = MethodTypeDesc.of(CD_JtdValidationResult);
71+
static final MethodTypeDesc MTD_JtdValidationResult_List = MethodTypeDesc.of(CD_JtdValidationResult, CD_List);
72+
static final MethodTypeDesc MTD_JtdValidationResult_JsonValue = MethodTypeDesc.of(CD_JtdValidationResult, CD_JsonValue);
73+
static final MethodTypeDesc MTD_void_String = MethodTypeDesc.of(ConstantDescs.CD_void, CD_String);
74+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package json.java21.jtd.codegen;
2+
3+
import java.lang.classfile.CodeBuilder;
4+
import java.lang.classfile.TypeKind;
5+
import java.lang.constant.ConstantDescs;
6+
7+
import json.java21.jtd.JtdSchema;
8+
9+
import static json.java21.jtd.codegen.Descriptors.*;
10+
11+
/// Emits bytecode for JTD Discriminator schema (tagged union).
12+
final class EmitDiscriminator {
13+
14+
private EmitDiscriminator() {}
15+
16+
static void emit(CodeBuilder cob, JtdSchema.DiscriminatorSchema d,
17+
int instSlot, int errSlot,
18+
String instPath, String schemaPath) {
19+
var end = cob.newLabel();
20+
21+
// Step 1: must be object
22+
var step1Fail = cob.newLabel();
23+
cob.aload(instSlot);
24+
cob.instanceOf(CD_JsonObject);
25+
cob.ifeq(step1Fail);
26+
27+
cob.aload(instSlot);
28+
cob.checkcast(CD_JsonObject);
29+
cob.invokeinterface(CD_JsonObject, "members", MTD_Map);
30+
int mapSlot = cob.allocateLocal(TypeKind.REFERENCE);
31+
cob.astore(mapSlot);
32+
33+
// Step 2: tag must exist
34+
var step2Fail = cob.newLabel();
35+
cob.aload(mapSlot);
36+
cob.ldc(d.discriminator());
37+
cob.invokeinterface(CD_Map, "containsKey", MTD_boolean_Object);
38+
cob.ifeq(step2Fail);
39+
40+
cob.aload(mapSlot);
41+
cob.ldc(d.discriminator());
42+
cob.invokeinterface(CD_Map, "get", MTD_Object_Object);
43+
cob.checkcast(CD_JsonValue);
44+
int tagValSlot = cob.allocateLocal(TypeKind.REFERENCE);
45+
cob.astore(tagValSlot);
46+
47+
// Step 3: tag must be string
48+
var step3Fail = cob.newLabel();
49+
cob.aload(tagValSlot);
50+
cob.instanceOf(CD_JsonString);
51+
cob.ifeq(step3Fail);
52+
53+
cob.aload(tagValSlot);
54+
cob.checkcast(CD_JsonString);
55+
cob.invokeinterface(CD_JsonString, "string", MTD_String);
56+
int tagStrSlot = cob.allocateLocal(TypeKind.REFERENCE);
57+
cob.astore(tagStrSlot);
58+
59+
// Step 4: dispatch to variants
60+
for (final var entry : d.mapping().entrySet()) {
61+
final var tagValue = entry.getKey();
62+
final var variantSchema = entry.getValue();
63+
var nextVariant = cob.newLabel();
64+
65+
cob.aload(tagStrSlot);
66+
cob.ldc(tagValue);
67+
cob.invokevirtual(CD_String, "equals", MTD_boolean_Object);
68+
cob.ifeq(nextVariant);
69+
70+
if (variantSchema instanceof JtdSchema.PropertiesSchema props) {
71+
EmitProperties.emit(cob, props, instSlot, errSlot, instPath,
72+
schemaPath + "/mapping/" + tagValue, d.discriminator());
73+
} else {
74+
EmitNode.emit(cob, variantSchema, instSlot, errSlot, instPath,
75+
schemaPath + "/mapping/" + tagValue);
76+
}
77+
cob.goto_(end);
78+
79+
cob.labelBinding(nextVariant);
80+
}
81+
82+
// Step 5: tag not in mapping
83+
EmitError.addError(cob, errSlot,
84+
instPath + "/" + d.discriminator(), schemaPath + "/mapping");
85+
cob.goto_(end);
86+
87+
// Error paths
88+
cob.labelBinding(step1Fail);
89+
EmitError.addError(cob, errSlot, instPath, schemaPath + "/discriminator");
90+
cob.goto_(end);
91+
92+
cob.labelBinding(step2Fail);
93+
EmitError.addError(cob, errSlot, instPath, schemaPath + "/discriminator");
94+
cob.goto_(end);
95+
96+
cob.labelBinding(step3Fail);
97+
EmitError.addError(cob, errSlot,
98+
instPath + "/" + d.discriminator(), schemaPath + "/discriminator");
99+
100+
cob.labelBinding(end);
101+
}
102+
103+
/// Dynamic-path variant: parent instancePath from local variable.
104+
static void emitDynamic(CodeBuilder cob, JtdSchema.DiscriminatorSchema d,
105+
int instSlot, int errSlot,
106+
int pathSlot, String schemaPath) {
107+
var end = cob.newLabel();
108+
109+
var step1Fail = cob.newLabel();
110+
cob.aload(instSlot);
111+
cob.instanceOf(CD_JsonObject);
112+
cob.ifeq(step1Fail);
113+
114+
cob.aload(instSlot);
115+
cob.checkcast(CD_JsonObject);
116+
cob.invokeinterface(CD_JsonObject, "members", MTD_Map);
117+
int mapSlot = cob.allocateLocal(TypeKind.REFERENCE);
118+
cob.astore(mapSlot);
119+
120+
var step2Fail = cob.newLabel();
121+
cob.aload(mapSlot);
122+
cob.ldc(d.discriminator());
123+
cob.invokeinterface(CD_Map, "containsKey", MTD_boolean_Object);
124+
cob.ifeq(step2Fail);
125+
126+
cob.aload(mapSlot);
127+
cob.ldc(d.discriminator());
128+
cob.invokeinterface(CD_Map, "get", MTD_Object_Object);
129+
cob.checkcast(CD_JsonValue);
130+
int tagValSlot = cob.allocateLocal(TypeKind.REFERENCE);
131+
cob.astore(tagValSlot);
132+
133+
var step3Fail = cob.newLabel();
134+
cob.aload(tagValSlot);
135+
cob.instanceOf(CD_JsonString);
136+
cob.ifeq(step3Fail);
137+
138+
cob.aload(tagValSlot);
139+
cob.checkcast(CD_JsonString);
140+
cob.invokeinterface(CD_JsonString, "string", MTD_String);
141+
int tagStrSlot = cob.allocateLocal(TypeKind.REFERENCE);
142+
cob.astore(tagStrSlot);
143+
144+
for (final var entry : d.mapping().entrySet()) {
145+
final var tagValue = entry.getKey();
146+
final var variantSchema = entry.getValue();
147+
var nextVariant = cob.newLabel();
148+
149+
cob.aload(tagStrSlot);
150+
cob.ldc(tagValue);
151+
cob.invokevirtual(CD_String, "equals", MTD_boolean_Object);
152+
cob.ifeq(nextVariant);
153+
154+
if (variantSchema instanceof JtdSchema.PropertiesSchema props) {
155+
EmitProperties.emitDynamic(cob, props, instSlot, errSlot, pathSlot,
156+
schemaPath + "/mapping/" + tagValue, d.discriminator());
157+
} else {
158+
EmitNode.emitDynamic(cob, variantSchema, instSlot, errSlot, pathSlot,
159+
schemaPath + "/mapping/" + tagValue);
160+
}
161+
cob.goto_(end);
162+
163+
cob.labelBinding(nextVariant);
164+
}
165+
166+
// tag not in mapping: error at pathSlot + "/" + discriminator
167+
cob.aload(pathSlot);
168+
cob.ldc("/" + d.discriminator());
169+
cob.invokevirtual(CD_String, "concat", MTD_String_String);
170+
int tagPathSlot = cob.allocateLocal(TypeKind.REFERENCE);
171+
cob.astore(tagPathSlot);
172+
EmitError.addErrorDynamic(cob, errSlot, tagPathSlot, schemaPath + "/mapping");
173+
cob.goto_(end);
174+
175+
cob.labelBinding(step1Fail);
176+
EmitError.addErrorDynamic(cob, errSlot, pathSlot, schemaPath + "/discriminator");
177+
cob.goto_(end);
178+
179+
cob.labelBinding(step2Fail);
180+
EmitError.addErrorDynamic(cob, errSlot, pathSlot, schemaPath + "/discriminator");
181+
cob.goto_(end);
182+
183+
cob.labelBinding(step3Fail);
184+
cob.aload(pathSlot);
185+
cob.ldc("/" + d.discriminator());
186+
cob.invokevirtual(CD_String, "concat", MTD_String_String);
187+
int tagPath2Slot = cob.allocateLocal(TypeKind.REFERENCE);
188+
cob.astore(tagPath2Slot);
189+
EmitError.addErrorDynamic(cob, errSlot, tagPath2Slot, schemaPath + "/discriminator");
190+
191+
cob.labelBinding(end);
192+
}
193+
}

0 commit comments

Comments
 (0)