diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 856db7dc00..da0cdd81b1 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -104,6 +104,30 @@ Load this file when changing anything under `java/` or when Java drives a cross-
- In `MemoryBuffer` and `MemoryOps` hot paths, duplicate small straight-line copy/read/write logic
when that keeps control flow direct. Do not add private helper indirection to hot paths just to
reduce local code duplication; keep helpers for slow, cold, or error paths.
+- In JDK 25 Fory JSON C2-sensitive code, preserve measured, naturally large hot-method boundaries.
+ A method that exceeds HotSpot's 325-byte hot-inline limit through real representation, scalar,
+ array, collection, or generated-schema work is an independent subtree owner. Generated group
+ planning counts only its invocation bytecodes, not the callee's transitive body. Do not shrink
+ such a method into a wrapper, add its body back into the caller budget, or manufacture a boundary
+ with padding, `@DontInline`, `CompileCommand`, fake receivers, or JVM flags. Keep escape,
+ malformed-input, Unicode, arbitrary-length, and other cold fallback work in separate methods.
+- `JsonTrampolineInvoke` is the generated UTF-8 writer body's single shared
+ `invokeinterface`-bytecode owner. Only fully constructed generated object bodies and member groups
+ held in final interface-typed fields may call it. Root facades, startup `ObjectCodec`s, handwritten
+ codecs, arrays, collections, maps, readers, and writers must not call it. A generated collection
+ loop calls the element codec's ordinary entry; a qualifying generated element entry reaches its
+ own object-body trampoline. The static helper remains tiny and may inline because the shared
+ `invokeinterface` profile, not helper size, creates the boundary. Do not create one trampoline per
+ generated class, pass concrete receivers, add dispatch/state/resolution work, or introduce
+ synthetic receivers; each of those breaks the shared real-receiver profile or its clean owner
+ model.
+- Intentional hot-path source duplication is required when helper extraction loses local
+ buffer/cursor state or makes C2 layout depend on compilation order. In particular, Long read/write
+ paths may repeat Int parsing or formatting logic, and unrolled array lanes may repeat complete
+ signed/range dispatch. Do not deduplicate these blocks without generated-source, `javap`,
+ PrintInlining, LogCompilation, nmethod, allocation, intrinsic, and aggregate evidence that the
+ independent boundary and performance remain stable. Preserve the nearby source comments that
+ record the owner and failure mode.
- In `MemoryBuffer` small-varint read/write hot paths, once Android has exited through the single
`MemoryOps` call, keep JVM bulk loads/stores local with raw Unsafe operations instead of routing
through branchful `_unsafeGet*` or `_unsafePut*` helpers. Add or preserve source comments that
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java
index d9ed4f2c6c..9317d7148c 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java
@@ -174,6 +174,8 @@ public byte[] toJsonBytes(Object value) {
writer.writeNull();
} else {
JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass());
+ // Root startup receivers must bypass the generated-only shared trampoline. Calling it
+ // here lets ObjectCodec dominate that BCI before generated body/group publication.
typeInfo.utf8Writer().writeUtf8(writer, value);
}
} finally {
@@ -223,6 +225,8 @@ public void writeJsonTo(Object value, OutputStream output) {
writer.writeNull();
} else {
JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass());
+ // Keep root dispatch direct; startup ObjectCodec receivers must not enter the
+ // generated body/group profile owned by JsonTrampolineInvoke.
typeInfo.utf8Writer().writeUtf8(writer, value);
}
} finally {
@@ -288,6 +292,8 @@ private byte[] toJsonBytesDeclared(Object value, Type type, Class> fallback) {
try {
state.typeResolver.lockJIT();
try {
+ // Declared root dispatch is still startup/root work. It must remain outside the
+ // generated-only JsonTrampolineInvoke receiver profile.
state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value);
} finally {
state.typeResolver.unlockJIT();
@@ -310,6 +316,8 @@ private void writeJsonDeclared(Object value, Type type, Class> fallback, Outpu
try {
state.typeResolver.lockJIT();
try {
+ // Declared root dispatch is direct for the same reason as toJsonBytesDeclared: only
+ // completed generated bodies and groups may contribute to the shared trampoline BCI.
state.rootTypeInfo(type, fallback).utf8Writer().writeUtf8(writer, value);
} finally {
state.typeResolver.unlockJIT();
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java
index c118415083..0e2e3a9b7c 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ClosedSubtypeCodec.java
@@ -39,10 +39,9 @@
* Resolver-local closed subtype dispatcher whose branch slots follow child JsonTypeInfo updates.
*
*
Inline discriminator state belongs to this parent. Any-readable children use parent-local
- * field tables and generated reader instances so one child can be shared by parents with different
- * discriminator names without changing the child's canonical metadata. Nested values of the same
- * child type still use its canonical reader; the derived skip table applies only to the outer
- * inline object.
+ * field tables and complete generated-reader arrays so one child can be shared by parents with
+ * different discriminator names without changing canonical child metadata. The derived skip table
+ * applies only to the outer inline object.
*
*
Writing rejects fixed-schema discriminator collisions when branches are resolved, but never
* queries an Any Map. Runtime dynamic-key conflicts are owned by the application; probing here
@@ -56,9 +55,9 @@ public final class ClosedSubtypeCodec implements JsonValueCodec {
private final JsonTypeInfo[] children;
private final ObjectCodec[] objectCodecs;
private JsonFieldTable[] inlineReadTables;
- private Latin1ReaderCodec[] inlineLatin1Readers;
- private Utf16ReaderCodec[] inlineUtf16Readers;
- private Utf8ReaderCodec[] inlineUtf8Readers;
+ private volatile Latin1ReaderCodec[] inlineLatin1Readers;
+ private volatile Utf16ReaderCodec[] inlineUtf16Readers;
+ private volatile Utf8ReaderCodec[] inlineUtf8Readers;
/** Creates an unresolved resolver-local dispatcher shell for a validated subtype definition. */
@Internal
@@ -85,7 +84,7 @@ public void resolve(JsonTypeResolver resolver) {
Class> subtype = definition.classes[i];
JsonTypeInfo child = resolver.getTypeInfo(subtype, subtype);
if (definition.inclusion == Inclusion.PROPERTY) {
- if (!child.usesDefaultObjectCodec()) {
+ if (resolver.canonicalObjectCodec(child) == null) {
throw new ForyJsonException(
"Inline JSON subtype requires the default object representation: " + subtype);
}
@@ -96,16 +95,15 @@ public void resolve(JsonTypeResolver resolver) {
if (any != null && (any.readField() != null || any.readSetter() != null)) {
if (inlineReadTables == null) {
inlineReadTables = new JsonFieldTable[children.length];
- inlineLatin1Readers = new Latin1ReaderCodec[children.length];
- inlineUtf16Readers = new Utf16ReaderCodec[children.length];
- inlineUtf8Readers = new Utf8ReaderCodec[children.length];
}
JsonFieldTable table =
objectCodec.readTable().withSkippedName(definition.scanInfo.property());
inlineReadTables[i] = table;
// The subtype scan restores the cursor, so the outer child rereads the discriminator and
- // needs this parent-local skip table. Nested child values must use the canonical table.
- resolver.resolveInlineAnyReaders(this, i, objectCodec, table);
+ // needs this parent-local skip table. The resolver constructs a complete immutable array
+ // of generated readers for each representation and publishes that array in one volatile
+ // write; never publish its elements independently. Nested child values keep the canonical
+ // table and canonical capability.
}
}
children[i] = child;
@@ -178,10 +176,11 @@ public Object readLatin1(Latin1JsonReader reader) {
int index = reader.scanObjectStringField(definition.scanInfo);
JsonFieldTable[] tables = inlineReadTables;
if (tables != null && tables[index] != null) {
- Latin1ReaderCodec codec = inlineLatin1Readers[index];
- return codec == null
- ? objectCodecs[index].readLatin1Object(reader, tables[index])
- : codec.readLatin1(reader);
+ Latin1ReaderCodec[] readers = inlineLatin1Readers;
+ if (readers != null) {
+ return readers[index].readLatin1(reader);
+ }
+ return objectCodecs[index].readLatin1Object(reader, tables[index]);
}
return children[index].latin1Reader().readLatin1(reader);
}
@@ -213,10 +212,11 @@ public Object readUtf16(Utf16JsonReader reader) {
int index = reader.scanObjectStringField(definition.scanInfo);
JsonFieldTable[] tables = inlineReadTables;
if (tables != null && tables[index] != null) {
- Utf16ReaderCodec codec = inlineUtf16Readers[index];
- return codec == null
- ? objectCodecs[index].readUtf16Object(reader, tables[index])
- : codec.readUtf16(reader);
+ Utf16ReaderCodec[] readers = inlineUtf16Readers;
+ if (readers != null) {
+ return readers[index].readUtf16(reader);
+ }
+ return objectCodecs[index].readUtf16Object(reader, tables[index]);
}
return children[index].utf16Reader().readUtf16(reader);
}
@@ -248,10 +248,11 @@ public Object readUtf8(Utf8JsonReader reader) {
int index = reader.scanObjectStringField(definition.scanInfo);
JsonFieldTable[] tables = inlineReadTables;
if (tables != null && tables[index] != null) {
- Utf8ReaderCodec codec = inlineUtf8Readers[index];
- return codec == null
- ? objectCodecs[index].readUtf8Object(reader, tables[index])
- : codec.readUtf8(reader);
+ Utf8ReaderCodec[] readers = inlineUtf8Readers;
+ if (readers != null) {
+ return readers[index].readUtf8(reader);
+ }
+ return objectCodecs[index].readUtf8Object(reader, tables[index]);
}
return children[index].utf8Reader().readUtf8(reader);
}
@@ -284,18 +285,71 @@ private int requireSubtype(Class> runtimeType) {
}
@Internal
- public void setInlineLatin1Reader(int index, Latin1ReaderCodec reader) {
- inlineLatin1Readers[index] = reader;
+ public int childCount() {
+ return children.length;
+ }
+
+ @Internal
+ public JsonTypeInfo child(int index) {
+ return children[index];
+ }
+
+ @Internal
+ public JsonFieldTable inlineReadTable(int index) {
+ return inlineReadTables == null ? null : inlineReadTables[index];
+ }
+
+ @Internal
+ public Latin1ReaderCodec[] inlineLatin1Readers() {
+ return inlineLatin1Readers;
+ }
+
+ @Internal
+ public Utf16ReaderCodec[] inlineUtf16Readers() {
+ return inlineUtf16Readers;
}
@Internal
- public void setInlineUtf16Reader(int index, Utf16ReaderCodec reader) {
- inlineUtf16Readers[index] = reader;
+ public Utf8ReaderCodec[] inlineUtf8Readers() {
+ return inlineUtf8Readers;
}
@Internal
- public void setInlineUtf8Reader(int index, Utf8ReaderCodec reader) {
- inlineUtf8Readers[index] = reader;
+ public void installInlineLatin1Readers(Latin1ReaderCodec[] readers) {
+ validateInlineReaders(readers);
+ if (inlineLatin1Readers != null) {
+ throw new IllegalStateException("Inline Latin1 readers are already installed");
+ }
+ inlineLatin1Readers = readers;
+ }
+
+ @Internal
+ public void installInlineUtf16Readers(Utf16ReaderCodec[] readers) {
+ validateInlineReaders(readers);
+ if (inlineUtf16Readers != null) {
+ throw new IllegalStateException("Inline UTF16 readers are already installed");
+ }
+ inlineUtf16Readers = readers;
+ }
+
+ @Internal
+ public void installInlineUtf8Readers(Utf8ReaderCodec[] readers) {
+ validateInlineReaders(readers);
+ if (inlineUtf8Readers != null) {
+ throw new IllegalStateException("Inline UTF8 readers are already installed");
+ }
+ inlineUtf8Readers = readers;
+ }
+
+ private void validateInlineReaders(Object[] readers) {
+ if (inlineReadTables == null || readers == null || readers.length != children.length) {
+ throw new IllegalArgumentException("Inline reader array does not match subtype branches");
+ }
+ for (int i = 0; i < children.length; i++) {
+ if (inlineReadTables[i] != null && readers[i] == null) {
+ throw new IllegalArgumentException("Missing inline reader for subtype branch " + i);
+ }
+ }
}
private static void rejectDiscriminatorCollision(ObjectCodec> codec, String property) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
index 4f527df8eb..ef046b9676 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
@@ -89,17 +89,23 @@ public static CollectionCodec> create(
Class> elementRawType = CodecUtils.rawType(elementType, Object.class);
CollectionFactory factory = collectionFactory(rawType, elementRawType);
JsonTypeInfo elementTypeInfo = resolver.getTypeInfo(elementType, elementRawType);
- return create(factory, elementTypeInfo);
+ return create(factory, elementTypeInfo, resolver.canonicalObjectCodec(elementTypeInfo) != null);
}
@Internal
public static CollectionCodec> create(
- Class> rawType, Class> elementRawType, JsonTypeInfo elementTypeInfo) {
- return create(collectionFactory(rawType, elementRawType), elementTypeInfo);
+ Class> rawType,
+ Class> elementRawType,
+ JsonTypeInfo elementTypeInfo,
+ JsonTypeResolver resolver) {
+ return create(
+ collectionFactory(rawType, elementRawType),
+ elementTypeInfo,
+ resolver.canonicalObjectCodec(elementTypeInfo) != null);
}
private static CollectionCodec> create(
- CollectionFactory factory, JsonTypeInfo elementTypeInfo) {
+ CollectionFactory factory, JsonTypeInfo elementTypeInfo, boolean objectElement) {
Object elementCodec = elementTypeInfo.stringWriter();
if (elementCodec == ScalarCodecs.StringCodec.INSTANCE) {
return new StringCollectionCodec(factory);
@@ -131,7 +137,7 @@ private static CollectionCodec> create(
if (elementCodec == ScalarCodecs.BigDecimalCodec.INSTANCE) {
return new BigDecimalCollectionCodec(factory);
}
- if (elementTypeInfo.usesDefaultObjectCodec()) {
+ if (objectElement) {
return new ObjectCollectionCodec(factory, elementTypeInfo);
}
return new GenericCollectionCodec(factory, elementTypeInfo);
@@ -195,7 +201,7 @@ final Collection> finishCollection(Collection collection) {
}
@Internal
- final boolean createsArrayList() {
+ public final boolean createsArrayList() {
return createsArrayList;
}
@@ -833,6 +839,11 @@ private ObjectCollectionCodec(CollectionFactory factory, JsonTypeInfo elementTyp
this.elementTypeInfo = elementTypeInfo;
}
+ @Internal
+ public JsonTypeInfo elementTypeInfo() {
+ return elementTypeInfo;
+ }
+
@Override
public void writeString(StringJsonWriter writer, Collection> value) {
if (value == null) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java
new file mode 100644
index 0000000000..8bcf1f8e51
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonTrampolineInvoke.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.codec;
+
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+
+/**
+ * Owns the single interface-invocation bytecode used by large generated UTF-8 writer receivers.
+ *
+ * This helper is deliberately not a general codec dispatcher. Generated object bodies and member
+ * groups store their completed receiver in a final interface-typed field and call this class.
+ * Consequently, every qualifying generated receiver contributes to the type profile of the same
+ * {@code invokeinterface} bytecode index instead of creating one profile per generated class. That
+ * real polymorphic profile prevents an outer generated collection or object from absorbing a
+ * receiver's complete transitive field closure before the receiver has formed its own level-4
+ * nmethod.
+ *
+ *
The static helper itself is intentionally tiny and may inline into generated callers. The
+ * boundary comes from the shared interface-call profile that remains after helper inlining, not
+ * from making this method large. Generated receivers own the real work and any naturally large
+ * method; this class owns only their common interface-call bytecode index.
+ *
+ *
Do not duplicate this helper per generated type, replace the interface field with a concrete
+ * receiver, or move root/handwritten codec calls through it. Root startup codecs execute before
+ * generated capabilities are published and would dominate the shared profile; concrete receiver
+ * types would let C2 recover and inline the exact implementation. Do not add type resolution,
+ * lifecycle state, allocation, callbacks, or fallback logic here. Those concerns belong to the
+ * resolver or the generated receiver, while this class owns only the shared invocation bytecode.
+ */
+@Internal
+public final class JsonTrampolineInvoke {
+ private JsonTrampolineInvoke() {}
+
+ /**
+ * Invokes generated UTF-8 object bodies and field groups through one interface-call BCI.
+ *
+ *
The interface invocation below must remain the only operation and the only {@code
+ * invokeinterface} bytecode in this method. The static helper call may itself be inlined, but
+ * HotSpot still consumes the one method-data profile owned by this bytecode index. The receivers
+ * must be real, fully constructed generated capabilities published only after construction; never
+ * add synthetic receivers merely to force polymorphism.
+ */
+ public static void writeUtf8(Utf8WriterCodec codec, Utf8JsonWriter writer, Object value) {
+ codec.writeUtf8(writer, value);
+ }
+}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
index 135006e139..7482edda68 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
@@ -22,13 +22,21 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
import org.apache.fory.annotation.Internal;
import org.apache.fory.codegen.CodeGenerator;
import org.apache.fory.codegen.CodegenContext;
import org.apache.fory.codegen.CompileUnit;
+import org.apache.fory.codegen.JaninoUtils;
import org.apache.fory.json.ForyJsonException;
+import org.apache.fory.json.codec.CodecUtils;
import org.apache.fory.json.codec.CollectionCodec;
import org.apache.fory.json.codec.JsonUnwrappedInfo;
import org.apache.fory.json.codec.Latin1ReaderCodec;
@@ -43,31 +51,35 @@
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldKind;
import org.apache.fory.json.resolver.JsonTypeInfo;
+import org.apache.fory.json.resolver.JsonTypeResolver;
/**
- * Shared generated-class owner for the five concrete object-codec capabilities.
+ * Generates concrete object and exact-collection capability classes.
*
- * One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. Separate
- * concurrent caches single-flight one generated class per Java type and capability, so using only
- * one input or output representation never generates the other paths. Expression construction,
- * source generation, and Janino compilation happen without a resolver-local JIT lock.
+ *
One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. The
+ * registry owns every generated-class future and single-flight decision. A resolver is passed only
+ * to the active source-generation call for short canonical metadata lookups; neither owner retains
+ * it.
*
- *
This class owns classes only. Resolver-local generated instances, child capability capture,
- * {@link JsonTypeInfo} slot installation, and generated parent-field updates belong to {@link
+ *
This class owns class generation only. Resolver-local generated instances, final direct-child
+ * capture, canonical cycle slots, and {@link JsonTypeInfo} slot installation belong to {@link
* org.apache.fory.json.resolver.JsonTypeResolver}. The raw types emitted for Janino stop at the
* generated source and constructor boundary; handwritten runtime capability APIs remain generic.
*/
public final class JsonCodegen {
+ // HotSpot JDK 25's measured hot-callsite bytecode ceiling. This is a local generated-method
+ // limit, not a transitive subtree estimate: once a concrete String/scalar/container callee owns
+ // a natural method larger than this limit, a generated group pays only the call bytecodes. Large
+ // generated groups cross the limit with real schema work and are then routed through the shared
+ // generated-only interface profile. Never use padding, annotations, or compiler directives to
+ // manufacture the boundary, and never add the already-independent callee body back to this
+ // planner's budget.
+ private static final int HOT_INLINE_LIMIT = 325;
private static final Map> ID_GENERATOR = new ConcurrentHashMap<>();
private final int codegenHash;
private final CodeGenerator codeGenerator;
private final ClassLoader jsonLoader;
- private final Map, Class>> stringWriterClasses = new ConcurrentHashMap<>();
- private final Map, Class>> utf8WriterClasses = new ConcurrentHashMap<>();
- private final Map, Class>> latin1ReaderClasses = new ConcurrentHashMap<>();
- private final Map, Class>> utf16ReaderClasses = new ConcurrentHashMap<>();
- private final Map, Class>> utf8ReaderClasses = new ConcurrentHashMap<>();
static String generatedCodecType(CodegenContext ctx, Class> codecType) {
// Janino-generated serializers use erased types, matching Fory core code generation. Runtime
@@ -89,56 +101,82 @@ public JsonCodegen(int codegenHash, ClassLoader jsonLoader) {
/**
* Compiles one concrete capability from fully resolved object metadata.
*
- * These compile methods run without a resolver-local JIT lock. Source-generation decisions may
- * inspect active codec classes only for non-default bindings, whose capability fields are never
- * replaced by generated raw-object codecs. Mutable default-object child capabilities are read
- * only by {@link org.apache.fory.json.resolver.JsonTypeResolver} while it constructs a
- * resolver-local instance under its JIT lock.
+ *
Source generation and Janino compilation are not enclosed by a resolver-local JIT lock.
+ * Canonical child metadata is read through short resolver-owned lookups; source shape never
+ * depends on mutable capability slots. Active codec classes are inspected only for non-canonical
+ * bindings, whose capability fields are never replaced by generated raw-object codecs.
*
- *
Generated classes are cached here because this object is shared by every pooled resolver of
- * one Fory JSON instance. Concurrent map computation provides generated-class single-flight;
- * resolver-local construction and capability publication belong to {@link
- * org.apache.fory.json.resolver.JsonTypeResolver} and are ordered by its generic {@link
- * JsonJITContext} callbacks.
+ *
The shared registry caches the resulting class future for every pooled resolver of one Fory
+ * JSON instance. Resolver-local construction and capability publication belong to {@link
+ * org.apache.fory.json.resolver.JsonTypeResolver} and are ordered by its {@link JsonJITContext}.
*/
@Internal
- public Class> compileStringWriter(ObjectCodec> codec) {
+ public Class> compileStringWriter(ObjectCodec> codec, JsonTypeResolver resolver) {
if (!canCompileWriter(codec)) {
return null;
}
- return stringWriterClasses.computeIfAbsent(codec.type(), ignored -> buildStringWriter(codec));
+ return buildStringWriter(codec, resolver);
}
@Internal
- public Class> compileUtf8Writer(ObjectCodec> codec) {
+ public Class> compileUtf8Writer(ObjectCodec> codec, JsonTypeResolver resolver) {
if (!canCompileWriter(codec)) {
return null;
}
- return utf8WriterClasses.computeIfAbsent(codec.type(), ignored -> buildUtf8Writer(codec));
+ return buildUtf8Writer(codec, resolver);
}
@Internal
- public Class> compileLatin1Reader(ObjectCodec> codec) {
+ public Class> compileLatin1Reader(ObjectCodec> codec, JsonTypeResolver resolver) {
if (!canCompileReader(codec)) {
return null;
}
- return latin1ReaderClasses.computeIfAbsent(codec.type(), ignored -> buildLatin1Reader(codec));
+ return buildLatin1Reader(codec, resolver);
}
@Internal
- public Class> compileUtf16Reader(ObjectCodec> codec) {
+ public Class> compileUtf16Reader(ObjectCodec> codec, JsonTypeResolver resolver) {
if (!canCompileReader(codec)) {
return null;
}
- return utf16ReaderClasses.computeIfAbsent(codec.type(), ignored -> buildUtf16Reader(codec));
+ return buildUtf16Reader(codec, resolver);
}
@Internal
- public Class> compileUtf8Reader(ObjectCodec> codec) {
+ public Class> compileUtf8Reader(
+ ObjectCodec> codec, JsonTypeResolver resolver, boolean finalDependencies) {
if (!canCompileReader(codec)) {
return null;
}
- return utf8ReaderClasses.computeIfAbsent(codec.type(), ignored -> buildUtf8Reader(codec));
+ return buildUtf8Reader(codec, resolver, finalDependencies);
+ }
+
+ @Internal
+ public Class> compileUtf8CollectionWriter(Type declaredType, CollectionCodec> owner) {
+ Class> rawType = CodecUtils.rawType(declaredType, Collection.class);
+ Class> elementType = CodecUtils.rawType(CodecUtils.elementType(declaredType), Object.class);
+ String generatedPackage = CodeGenerator.getPackage(elementType);
+ String className = className(elementType, simpleClassName(rawType) + "Utf8CollectionWriter");
+ boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec;
+ String code =
+ new Utf8CollectionWriterCodegen().genCode(generatedPackage, className, stringElements);
+ return compileCodecClass(generatedPackage, className, code);
+ }
+
+ @Internal
+ public Class> compileUtf8CollectionReader(Type declaredType, CollectionCodec> owner) {
+ if (!owner.createsArrayList()) {
+ throw new IllegalArgumentException(
+ "Generated UTF-8 collection requires an ArrayList binding");
+ }
+ Class> rawType = CodecUtils.rawType(declaredType, Collection.class);
+ Class> elementType = CodecUtils.rawType(CodecUtils.elementType(declaredType), Object.class);
+ String generatedPackage = CodeGenerator.getPackage(elementType);
+ String className = className(elementType, simpleClassName(rawType) + "Utf8CollectionReader");
+ boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec;
+ String code =
+ new Utf8CollectionReaderCodegen().genCode(generatedPackage, className, stringElements);
+ return compileCodecClass(generatedPackage, className, code);
}
@Internal
@@ -170,111 +208,434 @@ private String jitId(Class> type, String role) {
return qualifiedClassName(CodeGenerator.getPackage(type), className(type, role));
}
- private Class> buildStringWriter(ObjectCodec> codec) {
+ private Class> buildStringWriter(ObjectCodec> codec, JsonTypeResolver resolver) {
Class> type = codec.type();
String generatedPackage = CodeGenerator.getPackage(type);
String className = className(type, "StringWriter");
- JsonGeneratedCodecBuilder builder =
- new JsonGeneratedCodecBuilder(generatedPackage, className, type);
JsonUnwrappedInfo unwrapped = codec.unwrappedInfo();
if (unwrapped != null) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
String code =
- new StringWriterCodegen(this).genUnwrappedWriterCode(builder, type, codec, unwrapped);
+ new StringWriterCodegen(this, resolver)
+ .genUnwrappedWriterCode(builder, type, codec, unwrapped);
return compileCodecClass(generatedPackage, className, code);
}
AnyInfo any = codec.anyInfo();
- String code =
- any == null || any.writeField() == null && any.writeGetter() == null
- ? new StringWriterCodegen(this).genWriterCode(builder, type, codec.writeFields())
- : new StringWriterCodegen(this)
- .genAnyWriterCode(builder, type, codec.writeFields(), any);
- return compileCodecClass(generatedPackage, className, code);
+ JsonFieldInfo[] properties = codec.writeFields();
+ if (any != null && (any.writeField() != null || any.writeGetter() != null)) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ String code =
+ new StringWriterCodegen(this, resolver).genAnyWriterCode(builder, type, properties, any);
+ return compileCodecClass(generatedPackage, className, code);
+ }
+ Function source =
+ groupEnds -> {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ return new StringWriterCodegen(this, resolver)
+ .genWriterCode(builder, type, properties, groupEnds);
+ };
+ return compileWriterClass(
+ generatedPackage,
+ className,
+ properties,
+ "writeString",
+ "writeStringMembers",
+ false,
+ source);
}
- private Class> buildUtf8Writer(ObjectCodec> codec) {
+ private Class> buildUtf8Writer(ObjectCodec> codec, JsonTypeResolver resolver) {
Class> type = codec.type();
String generatedPackage = CodeGenerator.getPackage(type);
String className = className(type, "Utf8Writer");
- JsonGeneratedCodecBuilder builder =
- new JsonGeneratedCodecBuilder(generatedPackage, className, type);
JsonUnwrappedInfo unwrapped = codec.unwrappedInfo();
if (unwrapped != null) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
String code =
- new Utf8WriterCodegen(this).genUnwrappedWriterCode(builder, type, codec, unwrapped);
+ new Utf8WriterCodegen(this, resolver)
+ .genUnwrappedWriterCode(builder, type, codec, unwrapped);
return compileCodecClass(generatedPackage, className, code);
}
AnyInfo any = codec.anyInfo();
- String code =
- any == null || any.writeField() == null && any.writeGetter() == null
- ? new Utf8WriterCodegen(this).genWriterCode(builder, type, codec.writeFields())
- : new Utf8WriterCodegen(this).genAnyWriterCode(builder, type, codec.writeFields(), any);
- return compileCodecClass(generatedPackage, className, code);
+ JsonFieldInfo[] properties = codec.writeFields();
+ if (any != null && (any.writeField() != null || any.writeGetter() != null)) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ String code =
+ new Utf8WriterCodegen(this, resolver).genAnyWriterCode(builder, type, properties, any);
+ return compileCodecClass(generatedPackage, className, code);
+ }
+ Function source =
+ groupEnds -> {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ return new Utf8WriterCodegen(this, resolver)
+ .genWriterCode(builder, type, properties, groupEnds);
+ };
+ return compileWriterClass(
+ generatedPackage, className, properties, "writeUtf8", "writeUtf8Members", true, source);
}
- private Class> buildLatin1Reader(ObjectCodec> codec) {
+ private Class> buildLatin1Reader(ObjectCodec> codec, JsonTypeResolver resolver) {
Class> type = codec.type();
String generatedPackage = CodeGenerator.getPackage(type);
String className = className(type, "Latin1Reader");
- JsonGeneratedCodecBuilder builder =
- new JsonGeneratedCodecBuilder(generatedPackage, className, type);
JsonUnwrappedInfo unwrapped = codec.unwrappedInfo();
if (unwrapped != null) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
String code =
- new Latin1ReaderCodegen(this).genUnwrappedReaderCode(builder, type, codec, unwrapped);
+ new Latin1ReaderCodegen(this, resolver)
+ .genUnwrappedReaderCode(builder, type, codec, unwrapped);
return compileCodecClass(generatedPackage, className, code);
}
AnyInfo any = codec.anyInfo();
- String code =
- any == null || any.readField() == null && any.readSetter() == null
- ? new Latin1ReaderCodegen(this)
- .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo())
- : new Latin1ReaderCodegen(this)
- .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any);
- return compileCodecClass(generatedPackage, className, code);
+ JsonFieldInfo[] properties = codec.readFields();
+ Function source =
+ groupEnds -> {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ Latin1ReaderCodegen reader = new Latin1ReaderCodegen(this, resolver, groupEnds);
+ return any == null || any.readField() == null && any.readSetter() == null
+ ? reader.genReaderCode(builder, type, properties, codec.creatorInfo())
+ : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any);
+ };
+ return compileReaderClass(
+ generatedPackage,
+ className,
+ properties.length,
+ "readLatin1",
+ codec.creatorInfo() == null,
+ source);
}
- private Class> buildUtf16Reader(ObjectCodec> codec) {
+ private Class> buildUtf16Reader(ObjectCodec> codec, JsonTypeResolver resolver) {
Class> type = codec.type();
String generatedPackage = CodeGenerator.getPackage(type);
String className = className(type, "Utf16Reader");
- JsonGeneratedCodecBuilder builder =
- new JsonGeneratedCodecBuilder(generatedPackage, className, type);
JsonUnwrappedInfo unwrapped = codec.unwrappedInfo();
if (unwrapped != null) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
String code =
- new Utf16ReaderCodegen(this).genUnwrappedReaderCode(builder, type, codec, unwrapped);
+ new Utf16ReaderCodegen(this, resolver)
+ .genUnwrappedReaderCode(builder, type, codec, unwrapped);
return compileCodecClass(generatedPackage, className, code);
}
AnyInfo any = codec.anyInfo();
- String code =
- any == null || any.readField() == null && any.readSetter() == null
- ? new Utf16ReaderCodegen(this)
- .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo())
- : new Utf16ReaderCodegen(this)
- .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any);
- return compileCodecClass(generatedPackage, className, code);
+ JsonFieldInfo[] properties = codec.readFields();
+ Function source =
+ groupEnds -> {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ Utf16ReaderCodegen reader = new Utf16ReaderCodegen(this, resolver, groupEnds);
+ return any == null || any.readField() == null && any.readSetter() == null
+ ? reader.genReaderCode(builder, type, properties, codec.creatorInfo())
+ : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any);
+ };
+ return compileReaderClass(
+ generatedPackage,
+ className,
+ properties.length,
+ "readUtf16",
+ codec.creatorInfo() == null,
+ source);
}
- private Class> buildUtf8Reader(ObjectCodec> codec) {
+ private Class> buildUtf8Reader(
+ ObjectCodec> codec, JsonTypeResolver resolver, boolean finalDependencies) {
Class> type = codec.type();
String generatedPackage = CodeGenerator.getPackage(type);
String className = className(type, "Utf8Reader");
- JsonGeneratedCodecBuilder builder =
- new JsonGeneratedCodecBuilder(generatedPackage, className, type);
JsonUnwrappedInfo unwrapped = codec.unwrappedInfo();
if (unwrapped != null) {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
String code =
- new Utf8ReaderCodegen(this).genUnwrappedReaderCode(builder, type, codec, unwrapped);
+ new Utf8ReaderCodegen(this, resolver, finalDependencies)
+ .genUnwrappedReaderCode(builder, type, codec, unwrapped);
return compileCodecClass(generatedPackage, className, code);
}
AnyInfo any = codec.anyInfo();
- String code =
- any == null || any.readField() == null && any.readSetter() == null
- ? new Utf8ReaderCodegen(this)
- .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo())
- : new Utf8ReaderCodegen(this)
- .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any);
- return compileCodecClass(generatedPackage, className, code);
+ JsonFieldInfo[] properties = codec.readFields();
+ Function source =
+ groupEnds -> {
+ JsonGeneratedCodecBuilder builder =
+ new JsonGeneratedCodecBuilder(generatedPackage, className, type);
+ Utf8ReaderCodegen reader =
+ new Utf8ReaderCodegen(this, resolver, finalDependencies, groupEnds);
+ return any == null || any.readField() == null && any.readSetter() == null
+ ? reader.genReaderCode(builder, type, properties, codec.creatorInfo())
+ : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any);
+ };
+ return compileReaderClass(
+ generatedPackage,
+ className,
+ properties.length,
+ "readUtf8",
+ codec.creatorInfo() == null,
+ source);
+ }
+
+ private Class> compileReaderClass(
+ String generatedPackage,
+ String className,
+ int propertyCount,
+ String readMethod,
+ boolean groupable,
+ Function source) {
+ int[] groupEnds =
+ groupable
+ ? readerGroupEnds(generatedPackage, className, propertyCount, readMethod, source)
+ : oneGroup(propertyCount);
+ return compileCodecClass(generatedPackage, className, source.apply(groupEnds));
+ }
+
+ private Class> compileWriterClass(
+ String generatedPackage,
+ String className,
+ JsonFieldInfo[] properties,
+ String writeMethod,
+ String memberMethod,
+ boolean trampolineGroups,
+ Function source) {
+ if (properties.length < 2) {
+ return compileCodecClass(generatedPackage, className, source.apply(null));
+ }
+ if (trampolineGroups) {
+ // Measure the exact generated compilation unit. Stable leaf owners such as writeString and
+ // writeLongArray appear here only as calls; adding their transitive implementation cost would
+ // make schema grouping depend on C2's compilation order instead of generated bytecode.
+ // Small complete schemas remain direct. A large schema is split into real body/group
+ // receivers, and those receivers all reach the one JsonTrampolineInvoke interface BCI.
+ JaninoUtils.CodeStats directStats =
+ codeStats(generatedPackage, className, source.apply(null));
+ if (methodSize(directStats, writeMethod) <= HOT_INLINE_LIMIT) {
+ return compileCodecClass(generatedPackage, className, source.apply(null));
+ }
+ int firstGroupMember = JsonWriterCodegen.firstGroupMember(properties);
+ if (properties.length - firstGroupMember < 2) {
+ return compileCodecClass(generatedPackage, className, source.apply(null));
+ }
+ int[] groupEnds =
+ writerTrampolineGroupEnds(
+ generatedPackage,
+ className,
+ properties.length,
+ firstGroupMember,
+ writeMethod,
+ source);
+ if (groupEnds.length < 2) {
+ return compileCodecClass(generatedPackage, className, source.apply(null));
+ }
+ return compileCodecClass(generatedPackage, className, source.apply(groupEnds));
+ }
+ // Group only the bytecode emitted in this generated class. A callee with its own stable
+ // boundary contributes its invocation, not the body that C2 must keep in the callee.
+ int[] oneGroup = new int[] {properties.length};
+ JaninoUtils.CodeStats oneGroupStats =
+ codeStats(generatedPackage, className, source.apply(oneGroup));
+ if (privateMethodSize(oneGroupStats, writeMethod + "Object") <= HOT_INLINE_LIMIT) {
+ return compileCodecClass(generatedPackage, className, source.apply(null));
+ }
+ int[] groupEnds =
+ writerGroupEnds(
+ generatedPackage,
+ className,
+ properties.length,
+ JsonWriterCodegen.firstGroupMember(properties),
+ writeMethod,
+ memberMethod,
+ source);
+ return compileCodecClass(generatedPackage, className, source.apply(groupEnds));
+ }
+
+ private int[] writerTrampolineGroupEnds(
+ String generatedPackage,
+ String className,
+ int propertyCount,
+ int firstGroupMember,
+ String writeMethod,
+ Function source) {
+ // Start with declaration-order field ranges, compile the actual nested receiver classes, and
+ // merge a range forward while its own emitted method is still small enough to inline. The
+ // decision is made from local bytecode only: an independently compiled leaf contributes its
+ // call instruction, never a guessed recursive cost. Do not replace this with field-count,
+ // benchmark-shape, runtime-value, or transitive-subtree heuristics.
+ List ends = new ArrayList<>(propertyCount - firstGroupMember);
+ for (int end = firstGroupMember + 1; end <= propertyCount; end++) {
+ ends.add(end);
+ }
+ boolean merged;
+ do {
+ merged = false;
+ for (int group = 0; group < ends.size() - 1; group++) {
+ List candidate = new ArrayList<>(ends);
+ candidate.remove(group);
+ Map stats =
+ codeStatsByClass(generatedPackage, className, source.apply(toIntArray(candidate)));
+ if (nestedMethodSize(stats, JsonWriterCodegen.memberGroupClassName(group), writeMethod)
+ <= HOT_INLINE_LIMIT) {
+ ends = candidate;
+ merged = true;
+ break;
+ }
+ }
+ } while (merged);
+ return toIntArray(ends);
+ }
+
+ private int[] writerGroupEnds(
+ String generatedPackage,
+ String className,
+ int propertyCount,
+ int firstGroupMember,
+ String writeMethod,
+ String memberMethod,
+ Function source) {
+ List ends = new ArrayList<>(propertyCount - firstGroupMember);
+ for (int end = firstGroupMember + 1; end <= propertyCount; end++) {
+ ends.add(end);
+ }
+ if (ends.size() < 2) {
+ return oneGroup(propertyCount);
+ }
+ while (ends.size() > 1) {
+ int[] candidate = toIntArray(ends);
+ JaninoUtils.CodeStats stats = codeStats(generatedPackage, className, source.apply(candidate));
+ boolean merged = false;
+ for (int group = 0; group < ends.size() - 1; group++) {
+ String method = group == 0 ? memberMethod : memberMethod + group;
+ if (privateMethodSize(stats, method) <= HOT_INLINE_LIMIT) {
+ ends.remove(group);
+ merged = true;
+ break;
+ }
+ }
+ if (merged) {
+ continue;
+ }
+ return candidate;
+ }
+ return toIntArray(ends);
+ }
+
+ private int[] readerGroupEnds(
+ String generatedPackage,
+ String className,
+ int propertyCount,
+ String readMethod,
+ Function source) {
+ if (propertyCount < 2) {
+ return oneGroup(propertyCount);
+ }
+ // Child method bodies do not belong to their generated caller's bytecode budget. Compile the
+ // exact caller shape on this class-owned cold path, then merge declaration-order ranges until
+ // every emitted helper and the root that owns the final range naturally cross the hot-inline
+ // ceiling. Probe classes are never defined or dumped, so class publication and source shape
+ // remain independent of capability-slot timing.
+ List ends = new ArrayList<>(propertyCount);
+ for (int end = 1; end <= propertyCount; end++) {
+ ends.add(end);
+ }
+ while (ends.size() > 1) {
+ int[] candidate = toIntArray(ends);
+ JaninoUtils.CodeStats stats = codeStats(generatedPackage, className, source.apply(candidate));
+ int start = 0;
+ boolean merged = false;
+ for (int group = 0; group < ends.size() - 1; group++) {
+ if (methodSize(stats, readMethod + "Group" + start) <= HOT_INLINE_LIMIT) {
+ ends.remove(group);
+ merged = true;
+ break;
+ }
+ start = ends.get(group);
+ }
+ if (merged) {
+ continue;
+ }
+ if (methodSize(stats, readMethod) <= HOT_INLINE_LIMIT) {
+ ends.remove(ends.size() - 2);
+ continue;
+ }
+ return candidate;
+ }
+ return toIntArray(ends);
+ }
+
+ private JaninoUtils.CodeStats codeStats(String generatedPackage, String className, String code) {
+ Map stats = codeStatsByClass(generatedPackage, className, code);
+ return statsForMainClass(generatedPackage, className, stats);
+ }
+
+ private JaninoUtils.CodeStats statsForMainClass(
+ String generatedPackage, String className, Map stats) {
+ String classFile =
+ (generatedPackage.isEmpty() ? "" : generatedPackage.replace('.', '/') + "/")
+ + className
+ + ".class";
+ JaninoUtils.CodeStats classStats = stats.get(classFile);
+ if (classStats == null) {
+ throw new ForyJsonException("Missing generated JSON bytecode " + classFile);
+ }
+ return classStats;
+ }
+
+ private Map codeStatsByClass(
+ String generatedPackage, String className, String code) {
+ CompileUnit unit = new CompileUnit(generatedPackage, className, code);
+ Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit);
+ Map stats = new LinkedHashMap<>();
+ for (Map.Entry entry : classes.entrySet()) {
+ stats.put(entry.getKey(), JaninoUtils.getClassStats(entry.getValue()));
+ }
+ return stats;
+ }
+
+ private int nestedMethodSize(
+ Map stats, String nestedClass, String method) {
+ JaninoUtils.CodeStats match = null;
+ for (Map.Entry entry : stats.entrySet()) {
+ if (entry.getKey().endsWith(nestedClass + ".class")) {
+ if (match != null) {
+ throw new ForyJsonException("Ambiguous generated JSON class " + nestedClass);
+ }
+ match = entry.getValue();
+ }
+ }
+ if (match == null) {
+ throw new ForyJsonException("Missing generated JSON class " + nestedClass);
+ }
+ return methodSize(match, method);
+ }
+
+ private int methodSize(JaninoUtils.CodeStats stats, String method) {
+ Integer size = stats.methodsSize.get(method);
+ if (size == null) {
+ throw new ForyJsonException(
+ "Missing generated JSON method " + method + " in " + stats.methodsSize.keySet());
+ }
+ return size;
+ }
+
+ private int privateMethodSize(JaninoUtils.CodeStats stats, String sourceName) {
+ return methodSize(stats, sourceName + "$");
+ }
+
+ private int[] oneGroup(int propertyCount) {
+ return new int[] {propertyCount};
+ }
+
+ private int[] toIntArray(List values) {
+ int[] result = new int[values.size()];
+ for (int i = 0; i < values.size(); i++) {
+ result[i] = values.get(i);
+ }
+ return result;
}
private Class> compileCodecClass(String generatedPackage, String className, String code) {
@@ -434,11 +795,11 @@ private boolean canCompileAnyRead(AnyInfo any, boolean creator) {
return isVisible(any.valueRawType());
}
- Class> stringWriterFieldType(JsonTypeInfo typeInfo) {
+ Class> stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) {
if (typeInfo.usesAnnotationCodec()) {
return StringWriterCodec.class;
}
- if (typeInfo.usesDefaultObjectCodec()) {
+ if (resolver.canonicalObjectCodec(typeInfo) != null) {
return StringWriterCodec.class;
}
Object codec = typeInfo.stringWriter();
@@ -449,11 +810,14 @@ Class> stringWriterFieldType(JsonTypeInfo typeInfo) {
return StringWriterCodec.class;
}
- Class> utf8WriterFieldType(JsonTypeInfo typeInfo) {
+ Class> utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) {
if (typeInfo.usesAnnotationCodec()) {
return Utf8WriterCodec.class;
}
- if (typeInfo.usesDefaultObjectCodec()) {
+ if (resolver.exactUtf8WriterCollection(typeInfo) != null) {
+ return Utf8WriterCodec.class;
+ }
+ if (resolver.canonicalObjectCodec(typeInfo) != null) {
return Utf8WriterCodec.class;
}
Object codec = typeInfo.utf8Writer();
@@ -464,11 +828,11 @@ Class> utf8WriterFieldType(JsonTypeInfo typeInfo) {
return Utf8WriterCodec.class;
}
- Class> latin1ReaderFieldType(JsonTypeInfo typeInfo) {
+ Class> latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) {
if (typeInfo.usesAnnotationCodec()) {
return Latin1ReaderCodec.class;
}
- if (typeInfo.usesDefaultObjectCodec()) {
+ if (resolver.canonicalObjectCodec(typeInfo) != null) {
return Latin1ReaderCodec.class;
}
Class> type = typeInfo.latin1Reader().getClass();
@@ -478,11 +842,11 @@ Class> latin1ReaderFieldType(JsonTypeInfo typeInfo) {
return Latin1ReaderCodec.class;
}
- Class> utf16ReaderFieldType(JsonTypeInfo typeInfo) {
+ Class> utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) {
if (typeInfo.usesAnnotationCodec()) {
return Utf16ReaderCodec.class;
}
- if (typeInfo.usesDefaultObjectCodec()) {
+ if (resolver.canonicalObjectCodec(typeInfo) != null) {
return Utf16ReaderCodec.class;
}
Class> type = typeInfo.utf16Reader().getClass();
@@ -492,11 +856,15 @@ Class> utf16ReaderFieldType(JsonTypeInfo typeInfo) {
return Utf16ReaderCodec.class;
}
- Class> utf8ReaderFieldType(JsonTypeInfo typeInfo) {
+ Class> utf8ReaderFieldType(
+ JsonTypeInfo typeInfo, JsonTypeResolver resolver, boolean finalDependencies) {
if (typeInfo.usesAnnotationCodec()) {
return Utf8ReaderCodec.class;
}
- if (typeInfo.usesDefaultObjectCodec()) {
+ if (finalDependencies && resolver.exactUtf8Collection(typeInfo) != null) {
+ return Utf8ReaderCodec.class;
+ }
+ if (resolver.canonicalObjectCodec(typeInfo) != null) {
return Utf8ReaderCodec.class;
}
Class> type = typeInfo.utf8Reader().getClass();
@@ -507,10 +875,10 @@ Class> utf8ReaderFieldType(JsonTypeInfo typeInfo) {
}
@Internal
- public static Class> readNestedType(JsonFieldInfo property) {
+ public static Class> readNestedType(JsonFieldInfo property, JsonTypeResolver resolver) {
if (property.readKind() == JsonFieldKind.OBJECT
&& property.readRawType() != Object.class
- && property.readTypeInfo().usesDefaultObjectCodec()) {
+ && resolver.canonicalObjectCodec(property.readTypeInfo()) != null) {
return property.readRawType();
}
return null;
@@ -530,25 +898,35 @@ public static boolean usesWriteCodec(JsonFieldInfo property) {
}
}
+ @Internal
+ public static boolean usesUtf8WriteCodec(JsonFieldInfo property, JsonTypeResolver resolver) {
+ return usesWriteCodec(property)
+ || property.writeKind() == JsonFieldKind.COLLECTION
+ && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) != null;
+ }
+
static boolean writesStringCollectionDirectly(JsonFieldInfo property) {
return property.writeElementRawType() == String.class
&& property.writeTypeInfo().stringWriter().getClass()
== CollectionCodec.StringCollectionCodec.class;
}
- private static JsonTypeInfo writeObjectTypeInfo(JsonFieldInfo property) {
+ private static JsonTypeInfo writeObjectTypeInfo(
+ JsonFieldInfo property, JsonTypeResolver resolver) {
JsonTypeInfo typeInfo = property.writeTypeInfo();
- return usesWriteCodec(property) && typeInfo.usesDefaultObjectCodec() ? typeInfo : null;
+ return usesWriteCodec(property) && resolver.canonicalObjectCodec(typeInfo) != null
+ ? typeInfo
+ : null;
}
@Internal
- public static Class> writeNestedType(JsonFieldInfo property) {
- JsonTypeInfo typeInfo = writeObjectTypeInfo(property);
+ public static Class> writeNestedType(JsonFieldInfo property, JsonTypeResolver resolver) {
+ JsonTypeInfo typeInfo = writeObjectTypeInfo(property, resolver);
return typeInfo == null ? null : typeInfo.rawType();
}
@Internal
- public static boolean usesReadCodec(JsonFieldInfo property) {
+ public static boolean usesReadCodec(JsonFieldInfo property, JsonTypeResolver resolver) {
switch (property.readKind()) {
case ENUM:
case ARRAY:
@@ -556,36 +934,38 @@ public static boolean usesReadCodec(JsonFieldInfo property) {
case MAP:
return true;
case OBJECT:
- return !usesReadObjectCodec(property);
+ return !usesReadObjectCodec(property, resolver);
default:
return false;
}
}
- static boolean usesReadObjectCodec(JsonFieldInfo property) {
+ static boolean usesReadObjectCodec(JsonFieldInfo property, JsonTypeResolver resolver) {
return property.readKind() == JsonFieldKind.OBJECT
&& property.readRawType() != Object.class
- && property.readTypeInfo().usesDefaultObjectCodec();
+ && resolver.canonicalObjectCodec(property.readTypeInfo()) != null;
}
- static boolean storesReadObjectCodec(Class> type, JsonFieldInfo property) {
- Class> nestedType = readNestedType(property);
+ static boolean storesReadObjectCodec(
+ Class> type, JsonFieldInfo property, JsonTypeResolver resolver) {
+ Class> nestedType = readNestedType(property, resolver);
return nestedType != null && nestedType != type;
}
@Internal
- public static boolean storesSelfReader(ObjectCodec> owner) {
+ public static boolean storesSelfReader(ObjectCodec> owner, JsonTypeResolver resolver) {
AnyInfo any = owner.anyInfo();
if (any == null || any.readField() == null && any.readSetter() == null) {
return false;
}
- if (storesSelfReader(owner.type(), owner.readFields(), owner.creatorInfo() != null, any)) {
+ if (storesSelfReader(
+ owner.type(), owner.readFields(), owner.creatorInfo() != null, any, resolver)) {
return true;
}
JsonUnwrappedInfo unwrapped = owner.unwrappedInfo();
if (unwrapped != null) {
for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) {
- if (route.field() != null && readNestedType(route.field()) == owner.type()) {
+ if (route.field() != null && readNestedType(route.field(), resolver) == owner.type()) {
return true;
}
}
@@ -594,15 +974,19 @@ public static boolean storesSelfReader(ObjectCodec> owner) {
}
static boolean storesSelfReader(
- Class> type, JsonFieldInfo[] properties, boolean creator, AnyInfo any) {
- if (any.valueRawType() == type && any.valueTypeInfo().usesDefaultObjectCodec()) {
+ Class> type,
+ JsonFieldInfo[] properties,
+ boolean creator,
+ AnyInfo any,
+ JsonTypeResolver resolver) {
+ if (any.valueRawType() == type && resolver.canonicalObjectCodec(any.valueTypeInfo()) != null) {
return true;
}
if (creator) {
return false;
}
for (JsonFieldInfo property : properties) {
- if (readNestedType(property) == type) {
+ if (readNestedType(property, resolver) == type) {
return true;
}
}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java
index 1ddcdc9d20..ba1fd65a56 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonJITContext.java
@@ -19,125 +19,104 @@
package org.apache.fory.json.codegen;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.HashSet;
+import java.util.Set;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.fory.annotation.Internal;
-import org.apache.fory.codegen.CodeGenerator;
import org.apache.fory.util.ExceptionUtils;
-import org.apache.fory.util.Preconditions;
/**
- * Generic resolver-local lock and completion-notification context for generated code.
+ * Resolver-local lock and completion context for generated code.
*
- * This class deliberately has the same owner model as Fory core's JIT context. It knows nothing
- * about JSON readers, writers, capabilities, generated classes, or resolver metadata. A root graph
- * and its completion callbacks use the same local lock. The JIT action runs outside that lock;
- * success reacquires it before invoking the resolver-owned publication callback and then registered
- * parent notifications. Synchronous mode uses the callback map only to break recursive compilation
- * of the same identifier.
+ *
This class knows nothing about JSON readers, writers, capabilities, generated classes, or
+ * resolver metadata. The shared registry owns compilation and returns futures; this context only
+ * prevents a duplicate resolver-local publication request and invokes its completion while holding
+ * the same lock as root codec execution.
*
- *
{@link JITCallback#id()} correlates active child notifications; it is not a task-request or
- * deduplication key. Every asynchronous registration submits its action and owns its completion
- * callback. Duplicate actions converge on {@link JsonCodegen}'s shared generated-class cache, then
- * may construct and install equivalent resolver-local instances. This is intentional: generated
- * class compilation is single-flight, while resolver-local subscribers, construction, publication,
- * and generated parent-field updates are not deduplicated.
+ *
{@link JITCallback#id()} identifies one active resolver-local graph request. Generated-class
+ * single-flight belongs to the shared registry; resolver-local construction and atomic capability
+ * publication belong to the resolver callback.
*/
@Internal
public final class JsonJITContext {
private final boolean asyncCompilationEnabled;
- private final ExecutorService compilationService;
private final ReentrantLock jitLock;
- private final Map> callbacks;
- private int numRunningTasks;
+ private final Set activeTasks;
@Internal
- public JsonJITContext(boolean asyncCompilationEnabled, ExecutorService compilationService) {
+ public JsonJITContext(boolean asyncCompilationEnabled) {
this.asyncCompilationEnabled = asyncCompilationEnabled;
- this.compilationService = compilationService;
jitLock = new ReentrantLock(true);
- callbacks = new HashMap<>();
+ activeTasks = new HashSet<>();
}
- @Internal
- public T registerJITCallback(
- Callable interpretedAction, Callable jitAction, JITCallback callback) {
+ /**
+ * Registers one resolver-local publication against independently scheduled class futures.
+ *
+ * The future supplier must submit every independent compilation before composing its result.
+ * No compilation worker is used to wait for another worker. Repeated requests with the same ID
+ * reuse the active resolver-local request and return the interpreted capability until the
+ * complete result is published.
+ */
+ public void registerJITFuture(
+ Callable> futureAction, JITCallback callback) {
try {
lock();
+ Object id = callback.id();
+ if (!activeTasks.add(id)) {
+ return;
+ }
if (!asyncCompilationEnabled) {
- Object id = callback.id();
- if (callbacks.containsKey(id)) {
- return interpretedAction.call();
- }
- callbacks.put(id, new ArrayList<>());
try {
- T result = jitAction.call();
+ T result = futureAction.call().join();
callback.onSuccess(result);
- List notifyCallbacks = callbacks.get(id);
- for (int i = 0; i < notifyCallbacks.size(); i++) {
- notifyCallbacks.get(i).onNotifyResult(result);
- }
- return result;
} catch (Throwable t) {
- callback.onFailure(t);
- ExceptionUtils.throwException(t);
- throw new IllegalStateException("unreachable");
+ Throwable failure = unwrapFutureFailure(t);
+ callback.onFailure(failure);
+ ExceptionUtils.throwException(failure);
} finally {
- callbacks.remove(id);
+ activeTasks.remove(id);
}
+ return;
}
- // Do not skip registration when this ID is already present. JsonCodegen single-flights class
- // compilation, while every local registration must retain its own publication callback.
- callbacks.computeIfAbsent(callback.id(), ignored -> new ArrayList<>());
- numRunningTasks++;
- ExecutorService service = compilationService;
- if (service == null) {
- service = CodeGenerator.getCompilationService();
- }
+ CompletableFuture future;
try {
- service.execute(() -> runJITAction(jitAction, callback));
+ future = futureAction.call();
} catch (Throwable t) {
- finishTask();
+ activeTasks.remove(id);
callback.onFailure(t);
- ExceptionUtils.throwException(t);
+ return;
}
- return interpretedAction.call();
+ future.whenComplete(
+ (result, failure) -> {
+ if (failure == null) {
+ completeSuccess(callback, result);
+ } else {
+ completeFailure(callback, unwrapFutureFailure(failure));
+ }
+ });
} catch (Exception e) {
ExceptionUtils.throwException(e);
- throw new IllegalStateException("unreachable");
} finally {
unlock();
}
}
- private void runJITAction(Callable jitAction, JITCallback callback) {
- T result;
- try {
- result = jitAction.call();
- } catch (Throwable t) {
- completeFailure(callback, t);
- return;
- }
- completeSuccess(callback, result);
+ private static Throwable unwrapFutureFailure(Throwable failure) {
+ return failure instanceof CompletionException && failure.getCause() != null
+ ? failure.getCause()
+ : failure;
}
private void completeSuccess(JITCallback callback, T result) {
try {
lock();
callback.onSuccess(result);
- List notifyCallbacks = callbacks.get(callback.id());
- if (notifyCallbacks != null) {
- for (int i = 0; i < notifyCallbacks.size(); i++) {
- notifyCallbacks.get(i).onNotifyResult(result);
- }
- }
} finally {
- finishTask();
+ activeTasks.remove(callback.id());
unlock();
}
}
@@ -149,29 +128,7 @@ private void completeFailure(JITCallback callback, Throwable failure) {
lock();
callback.onFailure(failure);
} finally {
- finishTask();
- unlock();
- }
- }
-
- private void finishTask() {
- numRunningTasks--;
- if (numRunningTasks == 0) {
- callbacks.clear();
- }
- }
-
- public void registerJITNotifyCallback(Object id, NotifyCallback callback) {
- Preconditions.checkNotNull(id);
- try {
- lock();
- List notifyCallbacks = callbacks.get(id);
- if (notifyCallbacks == null) {
- callback.onNotifyMissed();
- } else {
- notifyCallbacks.add(callback);
- }
- } finally {
+ activeTasks.remove(callback.id());
unlock();
}
}
@@ -205,13 +162,4 @@ default void onFailure(Throwable failure) {
Object id();
}
-
- @Internal
- public interface NotifyCallback {
- default void onNotifyResult(Object result) {
- onNotifyMissed();
- }
-
- void onNotifyMissed();
- }
}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java
index 8a3766f89f..1fe645a63c 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java
@@ -44,6 +44,8 @@
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldKind;
import org.apache.fory.json.meta.JsonFieldTable;
+import org.apache.fory.json.resolver.JsonTypeInfo;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.memory.NativeByteOrder;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.type.TypeUtils;
@@ -54,25 +56,42 @@
* The three concrete generators own representation-specific token, field-name, enum, and direct
* value expressions. This base shares only source-construction algorithms after the concrete reader
* is selected; it is not a runtime reader mode. Generated readers retain immutable field lookup
- * metadata and concrete child capability fields, avoiding per-field resolver lookup. Wide objects
- * split generated methods to bound compiler size while preserving a single nullable capability
- * entry for each representation.
+ * metadata and final direct-child capabilities. Canonical multi-object cycles retain the final
+ * child type-info slot owner instead of a later-mutated capability field. Wide objects split
+ * generated methods to bound compiler size while preserving one capability entry per
+ * representation.
*/
abstract class JsonReaderCodegen {
- private static final int MIN_SPLIT_READ_FIELDS = 8;
- private static final int READ_FIELD_GROUP_SIZE = 2;
private static final int READ_FIELD_SWITCH_SIZE = 8;
private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN;
private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL;
private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL;
final JsonCodegen codegen;
+ final JsonTypeResolver resolver;
+ private final boolean finalDependencies;
+ private final int[] fastReadGroupEnds;
private AnyInfo any;
private Class> ownerType;
private boolean storesSelfReader;
- JsonReaderCodegen(JsonCodegen codegen) {
+ JsonReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) {
+ this(codegen, resolver, false, null);
+ }
+
+ JsonReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, boolean finalDependencies) {
+ this(codegen, resolver, finalDependencies, null);
+ }
+
+ JsonReaderCodegen(
+ JsonCodegen codegen,
+ JsonTypeResolver resolver,
+ boolean finalDependencies,
+ int[] fastReadGroupEnds) {
this.codegen = codegen;
+ this.resolver = resolver;
+ this.finalDependencies = finalDependencies;
+ this.fastReadGroupEnds = fastReadGroupEnds;
}
abstract Class> codecFieldType(JsonFieldInfo property);
@@ -85,6 +104,8 @@ abstract class JsonReaderCodegen {
abstract String readMethod();
+ abstract String readerSlotMethod();
+
abstract String readEnumMethod(boolean tokenValueRead, boolean hashFallback);
final String readEnumMethod(boolean tokenValueRead) {
@@ -113,7 +134,11 @@ abstract Expression readEnumField(
abstract Reference readerRef();
final Class> readNestedType(JsonFieldInfo property) {
- return JsonCodegen.readNestedType(property);
+ return JsonCodegen.readNestedType(property, resolver);
+ }
+
+ final boolean finalDependencies() {
+ return finalDependencies;
}
String genReaderCode(
@@ -125,25 +150,26 @@ String genReaderCode(
if (creatorInfo != null) {
return genCreatorReaderCode(builder, type, creatorInfo);
}
+ validateFastReadGroups(properties);
Class> readerType = readerType();
String readMethod = readMethod();
String slowMethod = readMethod + "Slow";
CodegenContext ctx = builder.context();
ctx.addImports(ObjectCodec.class, readerType, JsonFieldTable.class);
ctx.implementsInterfaces(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()));
- // Generated mutable readers retain immutable lookup metadata directly. Creator-backed readers
- // are selected above and consume the exact executable owned by JsonCreatorInfo.
+ // Generated readers retain immutable lookup metadata directly. Creator-backed readers are
+ // selected above and consume the exact executable owned by JsonCreatorInfo.
ctx.addField(JsonFieldTable.class, "readTable");
ctx.addField(long[].class, "fieldHashes");
for (int i = 0; i < properties.length; i++) {
if (usesReadInfo(properties[i])) {
ctx.addField(JsonFieldInfo.class, "rp" + i);
}
- if (JsonCodegen.usesReadCodec(properties[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(properties[i])), "r" + i);
+ if (JsonCodegen.usesReadCodec(properties[i], resolver)) {
+ addCapabilityField(ctx, codecFieldType(properties[i]), "r" + i);
}
if (storesReadObjectCodec(type, properties[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + i);
+ addObjectReaderField(ctx, properties[i], "o" + i);
}
}
addGeneratedConstructor(
@@ -160,10 +186,12 @@ String genReaderCode(
fastReadExpression(builder, readMethod, slowMethod, type, properties).genCode(ctx);
String bodyCode = body.code();
bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode);
+ // Generated object entries use the current-token fast path; concrete readers own whitespace
+ // fallback.
ctx.addMethod(
"@Override public final",
readMethod,
- "if (reader.tryReadNullToken()) {\n" + " return null;\n" + "}\n" + bodyCode,
+ "if (reader.tryReadNextNullToken()) {\n" + " return null;\n" + "}\n" + bodyCode,
Object.class,
readerType,
"reader");
@@ -173,6 +201,34 @@ String genReaderCode(
return ctx.genCode();
}
+ private void addCapabilityField(CodegenContext ctx, Class> type, String name) {
+ ctx.addField(finalDependencies, JsonCodegen.generatedCodecType(ctx, type), name, null);
+ }
+
+ private void addObjectReaderField(CodegenContext ctx, JsonFieldInfo field, String name) {
+ if (usesReaderSlot(field.readTypeInfo())) {
+ ctx.addField(true, ctx.type(JsonTypeInfo.class), name, null);
+ } else {
+ addCapabilityField(ctx, readerCapabilityType(), name);
+ }
+ }
+
+ private void addCreatorReaderField(CodegenContext ctx, JsonCreatorFieldInfo field, String name) {
+ if (usesReaderSlot(field.typeInfo())) {
+ ctx.addField(true, ctx.type(JsonTypeInfo.class), name, null);
+ } else {
+ addCapabilityField(ctx, readerCapabilityType(), name);
+ }
+ }
+
+ private void addAnyReaderField(CodegenContext ctx, AnyInfo any) {
+ if (usesReaderSlot(any.valueTypeInfo())) {
+ ctx.addField(true, ctx.type(JsonTypeInfo.class), "anyReader", null);
+ } else {
+ addCapabilityField(ctx, readerCapabilityType(), "anyReader");
+ }
+ }
+
String genAnyReaderCode(
JsonGeneratedCodecBuilder builder,
Class> type,
@@ -181,10 +237,12 @@ String genAnyReaderCode(
AnyInfo any) {
this.any = any;
ownerType = type;
- storesSelfReader = JsonCodegen.storesSelfReader(type, properties, creatorInfo != null, any);
+ storesSelfReader =
+ JsonCodegen.storesSelfReader(type, properties, creatorInfo != null, any, resolver);
if (creatorInfo != null) {
return genAnyCreatorReaderCode(builder, type, creatorInfo);
}
+ validateFastReadGroups(properties);
Class> concreteReaderType = readerType();
String readMethod = readMethod();
String anyReadMethod = readMethod + "Any";
@@ -198,7 +256,7 @@ String genAnyReaderCode(
addSelfReaderField(ctx);
boolean storesAnyReader = storesAnyReader(type);
if (storesAnyReader) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "anyReader");
+ addAnyReaderField(ctx, any);
}
if (any.readField() != null && Modifier.isFinal(any.readField().getModifiers())) {
addFinalAnyMapMethod(ctx);
@@ -207,33 +265,7 @@ String genAnyReaderCode(
addAnySetterMethod(ctx, type, any);
}
addReaderFields(ctx, type, properties);
- if (storesAnyReader) {
- addGeneratedConstructor(
- ctx,
- readerConstructorExpression(type, properties),
- ObjectCodec.class,
- "owner",
- JsonFieldTable.class,
- "readTable",
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
- "codecs",
- JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
- "anyReader");
- } else {
- addGeneratedConstructor(
- ctx,
- readerConstructorExpression(type, properties),
- ObjectCodec.class,
- "owner",
- JsonFieldTable.class,
- "readTable",
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
- "codecs");
- }
+ addAnyReaderConstructor(ctx, readerConstructorExpression(type, properties), storesAnyReader);
addGeneratedMethod(
ctx,
"private final",
@@ -245,7 +277,7 @@ String genAnyReaderCode(
ctx.addMethod(
"@Override public final",
readMethod,
- "if (reader.tryReadNullToken()) {\n return null;\n}\n"
+ "if (reader.tryReadNextNullToken()) {\n return null;\n}\n"
+ "return this."
+ anyReadMethod
+ "(reader);",
@@ -292,11 +324,11 @@ String genUnwrappedReaderCode(
if (rootCreator != null) {
ctx.addField(JsonCreatorInfo.class, "creator");
}
- storesSelfReader = JsonCodegen.storesSelfReader(owner);
+ storesSelfReader = JsonCodegen.storesSelfReader(owner, resolver);
addSelfReaderField(ctx);
boolean storesAnyReader = any != null && storesAnyReader(type);
if (storesAnyReader) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "anyReader");
+ addAnyReaderField(ctx, any);
}
if (any != null
&& any.readField() != null
@@ -311,7 +343,7 @@ String genUnwrappedReaderCode(
} else {
for (int i = 0; i < directCreatorFields.length; i++) {
if (!isDirectCreatorPrimitive(directCreatorFields[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + i);
+ addCreatorReaderField(ctx, directCreatorFields[i], "r" + i);
}
}
}
@@ -322,14 +354,14 @@ String genUnwrappedReaderCode(
if (usesReadInfo(field)) {
ctx.addField(JsonFieldInfo.class, "rp" + id);
}
- if (JsonCodegen.usesReadCodec(field)) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(field)), "r" + id);
+ if (JsonCodegen.usesReadCodec(field, resolver)) {
+ addCapabilityField(ctx, codecFieldType(field), "r" + id);
}
if (storesReadObjectCodec(type, field)) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + id);
+ addObjectReaderField(ctx, field, "o" + id);
}
} else if (!isDirectCreatorPrimitive(routes[i].creatorField())) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + id);
+ addCreatorReaderField(ctx, routes[i].creatorField(), "r" + id);
}
}
if (groups.length != 0) {
@@ -347,32 +379,8 @@ String genUnwrappedReaderCode(
directCount,
storesAnyReader,
any != null);
- if (storesAnyReader) {
- addGeneratedConstructor(
- ctx,
- constructor,
- ObjectCodec.class,
- "owner",
- JsonFieldTable.class,
- "readTable",
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
- "codecs",
- JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
- "anyReader");
- } else if (any != null) {
- addGeneratedConstructor(
- ctx,
- constructor,
- ObjectCodec.class,
- "owner",
- JsonFieldTable.class,
- "readTable",
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
- "codecs");
+ if (any != null) {
+ addAnyReaderConstructor(ctx, constructor, storesAnyReader);
} else {
addGeneratedConstructor(
ctx,
@@ -471,7 +479,7 @@ String genUnwrappedReaderCode(
ctx.addMethod(
"@Override public final",
readMethod(),
- "if (reader.tryReadNullToken()) {\n return null;\n}\n" + code,
+ "if (reader.tryReadNextNullToken()) {\n return null;\n}\n" + code,
Object.class,
readerType(),
"reader");
@@ -483,11 +491,11 @@ private void addReaderFields(CodegenContext ctx, Class> type, JsonFieldInfo[]
if (usesReadInfo(properties[i])) {
ctx.addField(JsonFieldInfo.class, "rp" + i);
}
- if (JsonCodegen.usesReadCodec(properties[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(properties[i])), "r" + i);
+ if (JsonCodegen.usesReadCodec(properties[i], resolver)) {
+ addCapabilityField(ctx, codecFieldType(properties[i]), "r" + i);
}
if (storesReadObjectCodec(type, properties[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "o" + i);
+ addObjectReaderField(ctx, properties[i], "o" + i);
}
}
}
@@ -555,7 +563,7 @@ private String genCreatorReaderCode(
ctx.addField(JsonCreatorInfo.class, "creator");
for (int i = 0; i < fields.length; i++) {
if (!isDirectCreatorPrimitive(fields[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + i);
+ addCreatorReaderField(ctx, fields[i], "r" + i);
}
}
addGeneratedConstructor(
@@ -575,7 +583,7 @@ private String genCreatorReaderCode(
ctx.addMethod(
"@Override public final",
readMethod(),
- "if (reader.tryReadNullToken()) {\n return null;\n}\n" + bodyCode,
+ "if (reader.tryReadNextNullToken()) {\n return null;\n}\n" + bodyCode,
Object.class,
concreteReaderType,
"reader");
@@ -602,40 +610,14 @@ private String genAnyCreatorReaderCode(
addSelfReaderField(ctx);
boolean storesAnyReader = storesAnyReader(type);
if (storesAnyReader) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "anyReader");
+ addAnyReaderField(ctx, any);
}
for (int i = 0; i < fields.length; i++) {
if (!isDirectCreatorPrimitive(fields[i])) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "r" + i);
+ addCreatorReaderField(ctx, fields[i], "r" + i);
}
}
- if (storesAnyReader) {
- addGeneratedConstructor(
- ctx,
- creatorConstructorExpression(fields),
- ObjectCodec.class,
- "owner",
- JsonFieldTable.class,
- "readTable",
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
- "codecs",
- JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
- "anyReader");
- } else {
- addGeneratedConstructor(
- ctx,
- creatorConstructorExpression(fields),
- ObjectCodec.class,
- "owner",
- JsonFieldTable.class,
- "readTable",
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
- "codecs");
- }
+ addAnyReaderConstructor(ctx, creatorConstructorExpression(fields), storesAnyReader);
addCreatorMethod(ctx, type, creatorInfo.executable());
addGeneratedMethod(
ctx,
@@ -648,7 +630,7 @@ private String genAnyCreatorReaderCode(
ctx.addMethod(
"@Override public final",
readMethod,
- "if (reader.tryReadNullToken()) {\n return null;\n}\n"
+ "if (reader.tryReadNextNullToken()) {\n return null;\n}\n"
+ "return this."
+ anyReadMethod
+ "(reader);",
@@ -818,24 +800,74 @@ private Expression creatorConstructorExpression(JsonCreatorFieldInfo[] fields) {
new Reference("this.readTable", TypeRef.of(JsonFieldTable.class)),
new Reference("readTable", TypeRef.of(JsonFieldTable.class))));
if (storesAnyReader(ownerType)) {
- expressions.add(
- new Expression.Assign(
- new Reference("this.anyReader", TypeRef.of(readerCapabilityType())),
- new Reference("anyReader", TypeRef.of(readerCapabilityType()))));
+ addAnyReaderAssignment(expressions, owner);
}
}
+ addSelfReaderAssignment(expressions);
Reference codecs = new Reference("codecs", TypeRef.of(readerArrayType()));
for (int i = 0; i < fields.length; i++) {
if (!isDirectCreatorPrimitive(fields[i])) {
- expressions.add(
- new Expression.Assign(
- new Reference("this.r" + i, TypeRef.of(readerCapabilityType())),
- new Expression.ArrayValue(codecs, Expression.Literal.ofInt(i))));
+ if (usesReaderSlot(fields[i].typeInfo())) {
+ Expression creator =
+ new Expression.Invoke(owner, "creatorInfo", TypeRef.of(JsonCreatorInfo.class))
+ .inline();
+ Expression creatorFields =
+ new Expression.Invoke(creator, "fields", TypeRef.of(JsonCreatorFieldInfo[].class))
+ .inline();
+ Expression field = new Expression.ArrayValue(creatorFields, Expression.Literal.ofInt(i));
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.r" + i, TypeRef.of(JsonTypeInfo.class)),
+ new Expression.Invoke(field, "typeInfo", TypeRef.of(JsonTypeInfo.class))
+ .inline()));
+ } else {
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.r" + i, TypeRef.of(readerCapabilityType())),
+ new Expression.ArrayValue(codecs, Expression.Literal.ofInt(i))));
+ }
}
}
return expressions;
}
+ private void addAnyReaderAssignment(Expression.ListExpression expressions, Expression owner) {
+ if (usesReaderSlot(any.valueTypeInfo())) {
+ Expression anyInfo =
+ new Expression.Invoke(owner, "anyInfo", TypeRef.of(AnyInfo.class)).inline();
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.anyReader", TypeRef.of(JsonTypeInfo.class)),
+ new Expression.Invoke(anyInfo, "valueTypeInfo", TypeRef.of(JsonTypeInfo.class))
+ .inline()));
+ } else {
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.anyReader", TypeRef.of(readerCapabilityType())),
+ new Reference("anyReader", TypeRef.of(readerCapabilityType()))));
+ }
+ }
+
+ private void addSelfReaderAssignment(Expression.ListExpression expressions) {
+ if (!storesSelfReader) {
+ return;
+ }
+ Expression nestedReader = selfRef();
+ if (any != null) {
+ Reference supplied = new Reference("selfReader", TypeRef.of(readerCapabilityType()));
+ nestedReader =
+ new Expression.Ternary(
+ eq(supplied, new Expression.Null(TypeRef.of(readerCapabilityType()), false)),
+ selfRef(),
+ supplied,
+ true,
+ TypeRef.of(readerCapabilityType()));
+ }
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.selfReader", TypeRef.of(readerCapabilityType())), nestedReader));
+ }
+
private Expression creatorReadExpression(Class> type, JsonCreatorInfo creatorInfo) {
JsonCreatorFieldInfo[] fields = creatorInfo.fields();
Expression.ListExpression expressions = new Expression.ListExpression();
@@ -1009,12 +1041,12 @@ private Expression creatorDefault(Class> type) {
private Expression readCreatorValue(JsonCreatorFieldInfo field, int id) {
Class> type = field.rawType();
if (!isDirectCreatorPrimitive(field)) {
+ Expression codec =
+ usesReaderSlot(field.typeInfo())
+ ? readerFromSlot(fieldRef("r" + id, JsonTypeInfo.class))
+ : fieldRef("r" + id, readerCapabilityType());
Expression value =
- new Expression.Invoke(
- fieldRef("r" + id, readerCapabilityType()),
- readMethod(),
- TypeRef.of(Object.class),
- readerRef());
+ new Expression.Invoke(codec, readMethod(), TypeRef.of(Object.class), readerRef());
if (type.isPrimitive()) {
value =
new Expression.StaticInvoke(
@@ -1079,11 +1111,12 @@ private void addFastReadGroupMethods(
Class> readerType,
Class> type,
JsonFieldInfo[] properties) {
- if (!shouldSplitFastRead(properties)) {
+ if (!shouldSplitFastRead()) {
return;
}
- for (int start = 0; start < properties.length; ) {
- int end = readGroupEnd(properties, start);
+ int start = 0;
+ for (int group = 0; group < fastReadGroupEnds.length - 1; group++) {
+ int end = fastReadGroupEnds[group];
if (any == null) {
addGeneratedMethod(
ctx,
@@ -1249,6 +1282,41 @@ private void addGeneratedConstructor(
ctx.addConstructor(code, params);
}
+ private void addAnyReaderConstructor(
+ CodegenContext ctx, Expression expression, boolean storesAnyReader) {
+ if (storesAnyReader) {
+ addGeneratedConstructor(
+ ctx,
+ expression,
+ ObjectCodec.class,
+ "owner",
+ JsonFieldTable.class,
+ "readTable",
+ JsonFieldInfo[].class,
+ "properties",
+ JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
+ "codecs",
+ JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
+ "selfReader",
+ JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
+ "anyReader");
+ } else {
+ addGeneratedConstructor(
+ ctx,
+ expression,
+ ObjectCodec.class,
+ "owner",
+ JsonFieldTable.class,
+ "readTable",
+ JsonFieldInfo[].class,
+ "properties",
+ JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()),
+ "codecs",
+ JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
+ "selfReader");
+ }
+ }
+
private Expression readerConstructorExpression(Class> type, JsonFieldInfo[] properties) {
Expression.ListExpression expressions = new Expression.ListExpression();
Reference propertiesRef = new Reference("properties", TypeRef.of(JsonFieldInfo[].class));
@@ -1265,12 +1333,10 @@ private Expression readerConstructorExpression(Class> type, JsonFieldInfo[] pr
expressions.add(
new Expression.Assign(new Reference("this.owner", TypeRef.of(ObjectCodec.class)), owner));
}
+ addSelfReaderAssignment(expressions);
if (any != null) {
if (storesAnyReader(ownerType)) {
- expressions.add(
- new Expression.Assign(
- new Reference("this.anyReader", TypeRef.of(readerCapabilityType())),
- new Reference("anyReader", TypeRef.of(readerCapabilityType()))));
+ addAnyReaderAssignment(expressions, owner);
}
}
Reference hashes = new Reference("this.fieldHashes", TypeRef.of(long[].class));
@@ -1295,7 +1361,7 @@ private Expression readerConstructorExpression(Class> type, JsonFieldInfo[] pr
hashes,
new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)).inline(),
id));
- if (JsonCodegen.usesReadCodec(properties[i])) {
+ if (JsonCodegen.usesReadCodec(properties[i], resolver)) {
Class> codecType = codecFieldType(properties[i]);
expressions.add(
new Expression.Assign(
@@ -1303,10 +1369,18 @@ private Expression readerConstructorExpression(Class> type, JsonFieldInfo[] pr
new Expression.Cast(
new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType))));
} else if (storesReadObjectCodec(type, properties[i])) {
- expressions.add(
- new Expression.Assign(
- new Reference("this.o" + i, TypeRef.of(readerCapabilityType())),
- new Expression.ArrayValue(codecsRef, id)));
+ if (usesReaderSlot(properties[i].readTypeInfo())) {
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.o" + i, TypeRef.of(JsonTypeInfo.class)),
+ new Expression.Invoke(property, "readTypeInfo", TypeRef.of(JsonTypeInfo.class))
+ .inline()));
+ } else {
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.o" + i, TypeRef.of(readerCapabilityType())),
+ new Expression.ArrayValue(codecsRef, id)));
+ }
}
}
return expressions;
@@ -1338,6 +1412,7 @@ private Expression unwrappedReaderConstructor(
unwrapped,
new Expression.Invoke(owner, "unwrappedInfo", TypeRef.of(JsonUnwrappedInfo.class))
.inline()));
+ addSelfReaderAssignment(expressions);
if (creatorFields != null) {
expressions.add(
new Expression.Assign(
@@ -1414,7 +1489,7 @@ private void addUnwrappedReaderAssignment(
new Expression.Assign(
new Reference("this.rp" + id, TypeRef.of(JsonFieldInfo.class)), property));
}
- if (JsonCodegen.usesReadCodec(field)) {
+ if (JsonCodegen.usesReadCodec(field, resolver)) {
Class> codecType = codecFieldType(field);
expressions.add(
new Expression.Assign(
@@ -2098,7 +2173,7 @@ private Expression fastReadExpression(
String slowMethod,
Class> type,
JsonFieldInfo[] properties) {
- if (shouldSplitFastRead(properties)) {
+ if (shouldSplitFastRead()) {
return splitFastReadExpression(builder, readMethod, slowMethod, type, properties);
}
Expression object = objectExpression(builder);
@@ -2154,12 +2229,23 @@ private Expression splitFastReadExpression(
Expression hashes =
new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class));
expressions.add(hashes);
- for (int start = 0; start < properties.length; ) {
- int end = readGroupEnd(properties, start);
+ int start = 0;
+ for (int group = 0; group < fastReadGroupEnds.length - 1; group++) {
+ int end = fastReadGroupEnds[group];
Expression groupCall = inline(readGroupCall(readMethod, start, object, hashes));
expressions.add(new Expression.If(not(groupCall), returnObject(object)));
start = end;
}
+ Expression[] skips = new Expression[properties.length];
+ for (int i = start + 1; i < properties.length; i++) {
+ skips[i] = new Expression.Variable("skip" + i, Expression.Literal.False);
+ expressions.add(skips[i]);
+ }
+ for (int i = start; i < properties.length; i++) {
+ Expression read =
+ fastReadField(builder, slowMethod, type, properties, i, object, hashes, skips);
+ expressions.add(i == start ? read : new Expression.If(not(skips[i]), read));
+ }
expressions.add(returnObject(object));
return expressions;
}
@@ -2184,11 +2270,7 @@ private Expression fastReadGroupExpression(
fastReadField(builder, slowMethod, type, properties, i, end, true, object, hashes, skips);
expressions.add(i == start ? read : new Expression.If(not(skips[i]), read));
}
- if (end < properties.length) {
- expressions.add(returnTrue());
- } else {
- expressions.add(returnFalse());
- }
+ expressions.add(returnTrue());
return expressions;
}
@@ -2457,47 +2539,34 @@ final long packedNameMask(int length) {
return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L;
}
- final boolean shouldSplitFastRead(JsonFieldInfo[] properties) {
- return properties.length >= MIN_SPLIT_READ_FIELDS;
- }
-
- final String readGroupMethod(String readMethod, int start) {
- return readMethod + "Group" + start;
- }
-
- final int readGroupEnd(JsonFieldInfo[] properties, int start) {
- int end = start + 1;
- while (end < properties.length
- && end - start < READ_FIELD_GROUP_SIZE
- && canPairReadFields(properties[end - 1], properties[end])) {
- end++;
+ private void validateFastReadGroups(JsonFieldInfo[] properties) {
+ if (fastReadGroupEnds == null || fastReadGroupEnds.length == 0) {
+ throw new IllegalArgumentException("Fast-read group boundaries are required");
}
- return end;
- }
-
- final boolean canPairReadFields(JsonFieldInfo left, JsonFieldInfo right) {
- JsonFieldKind leftKind = left.readKind();
- JsonFieldKind rightKind = right.readKind();
- if (leftKind == null || rightKind == null) {
- return false;
- }
- // Fast-read fallback branches duplicate some field reads in each group. Keep method-size
- // estimation conservative so generated helpers stay close to the C2-friendly target.
- if (leftKind == JsonFieldKind.ENUM
- || rightKind == JsonFieldKind.ENUM
- || leftKind == JsonFieldKind.COLLECTION
- || rightKind == JsonFieldKind.COLLECTION
- || leftKind == JsonFieldKind.MAP
- || rightKind == JsonFieldKind.MAP) {
- return false;
+ if (properties.length == 0) {
+ if (fastReadGroupEnds.length != 1 || fastReadGroupEnds[0] != 0) {
+ throw new IllegalArgumentException("Empty readers require one empty group");
+ }
+ return;
}
- if (leftKind == JsonFieldKind.ARRAY || rightKind == JsonFieldKind.ARRAY) {
- return false;
+ int previous = 0;
+ for (int end : fastReadGroupEnds) {
+ if (end <= previous || end > properties.length) {
+ throw new IllegalArgumentException("Invalid fast-read group boundary " + end);
+ }
+ previous = end;
}
- if (leftKind == JsonFieldKind.OBJECT || rightKind == JsonFieldKind.OBJECT) {
- return leftKind == JsonFieldKind.BOOLEAN || rightKind == JsonFieldKind.BOOLEAN;
+ if (previous != properties.length) {
+ throw new IllegalArgumentException("Fast-read groups must cover every property");
}
- return true;
+ }
+
+ final boolean shouldSplitFastRead() {
+ return fastReadGroupEnds.length > 1;
+ }
+
+ final String readGroupMethod(String readMethod, int start) {
+ return readMethod + "Group" + start;
}
final boolean shouldSplitFieldSwitch(JsonFieldInfo[] properties) {
@@ -3033,13 +3102,10 @@ final Reference selfRef() {
private void addSelfReaderField(CodegenContext ctx) {
if (storesSelfReader) {
- // Canonical readers keep this self-reference. A parent-local inline instance is rewired to
- // the canonical reader before publication so its derived skip table cannot reach children.
+ // Canonical construction supplies null and stores this. A parent-local inline construction
+ // supplies the final canonical reader so its derived skip table cannot reach nested values.
ctx.addField(
- false,
- JsonCodegen.generatedCodecType(ctx, readerCapabilityType()),
- "selfReader",
- selfRef());
+ true, JsonCodegen.generatedCodecType(ctx, readerCapabilityType()), "selfReader", null);
}
}
@@ -3048,13 +3114,26 @@ private Expression nestedSelfReaderRef() {
}
private boolean storesAnyReader(Class> type) {
- return !any.valueTypeInfo().usesDefaultObjectCodec() || any.valueTypeInfo().rawType() != type;
+ return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null
+ || any.valueTypeInfo().rawType() != type;
}
private Expression anyReaderRef() {
- return storesAnyReader(ownerType)
- ? fieldRef("anyReader", readerCapabilityType())
- : nestedSelfReaderRef();
+ if (!storesAnyReader(ownerType)) {
+ return nestedSelfReaderRef();
+ }
+ return usesReaderSlot(any.valueTypeInfo())
+ ? readerFromSlot(fieldRef("anyReader", JsonTypeInfo.class))
+ : fieldRef("anyReader", readerCapabilityType());
+ }
+
+ private boolean usesReaderSlot(JsonTypeInfo child) {
+ return resolver.usesReaderSlot(ownerType, child);
+ }
+
+ private Expression readerFromSlot(Expression slot) {
+ return new Expression.Invoke(slot, readerSlotMethod(), TypeRef.of(readerCapabilityType()))
+ .inline();
}
final Reference fieldRef(String name, Class> type) {
@@ -3462,7 +3541,7 @@ final boolean usesReadInfo(JsonFieldInfo property) {
final boolean usesReadObjectCodec(JsonFieldInfo property) {
return property.readKind() == JsonFieldKind.OBJECT
&& property.readRawType() != Object.class
- && property.readTypeInfo().usesDefaultObjectCodec();
+ && resolver.canonicalObjectCodec(property.readTypeInfo()) != null;
}
final boolean storesReadObjectCodec(Class> type, JsonFieldInfo property) {
@@ -3644,7 +3723,7 @@ final Expression readObject(
int id,
Expression object) {
if (property.readRawType() == Object.class
- || !property.readTypeInfo().usesDefaultObjectCodec()) {
+ || resolver.canonicalObjectCodec(property.readTypeInfo()) == null) {
return readResolvedField(builder, property, id, object);
}
return builder.setField(property, object, readObjectValue(type, property, id));
@@ -3715,7 +3794,9 @@ final Expression readObjectValue(Class> type, JsonFieldInfo property, int id)
Expression codec =
property.readRawType() == type
? nestedSelfReaderRef()
- : fieldRef("o" + id, readerCapabilityType());
+ : usesReaderSlot(property.readTypeInfo())
+ ? readerFromSlot(fieldRef("o" + id, JsonTypeInfo.class))
+ : fieldRef("o" + id, readerCapabilityType());
return new Expression.Cast(
inline(
new Expression.Invoke(
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java
index d892bb3909..10342ce835 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java
@@ -31,11 +31,13 @@
import java.util.List;
import java.util.Map;
import org.apache.fory.codegen.Code;
+import org.apache.fory.codegen.CodeGenerator;
import org.apache.fory.codegen.CodegenContext;
import org.apache.fory.codegen.Expression;
import org.apache.fory.codegen.Expression.Reference;
import org.apache.fory.codegen.ExpressionOptimizer;
import org.apache.fory.json.ForyJsonException;
+import org.apache.fory.json.codec.JsonTrampolineInvoke;
import org.apache.fory.json.codec.JsonUnwrappedInfo;
import org.apache.fory.json.codec.JsonUnwrappedInfo.Group;
import org.apache.fory.json.codec.JsonUnwrappedInfo.WriteEntry;
@@ -43,6 +45,8 @@
import org.apache.fory.json.codec.ObjectCodec.AnyInfo;
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldKind;
+import org.apache.fory.json.resolver.JsonTypeInfo;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.reflect.TypeRef;
/**
@@ -51,8 +55,9 @@
*
The concrete generators own representation-specific field prefixes, scalar stores, and child
* capability types. This base shares source-construction algorithms only after the concrete writer
* is selected; it is not a runtime output mode. Generated writers retain precomputed field tokens
- * and concrete child capabilities, fuse safe object prefixes, and split wide objects into bounded
- * methods to protect compiler and inlining budgets without adding per-field dispatch.
+ * and final direct-child capabilities; canonical multi-object cycles retain the final child
+ * type-info slot owner. They fuse safe object prefixes and split ordinary object writers from their
+ * actual emitted bytecode without adding per-field resolver lookup.
*/
abstract class JsonWriterCodegen {
// Bound field logic in independently compiled generated methods. The entry method keeps a small
@@ -61,10 +66,12 @@ abstract class JsonWriterCodegen {
private static final int INLINE_TAIL_MEMBERS = 4;
final JsonCodegen codegen;
+ final JsonTypeResolver resolver;
private Class> ownerType;
- JsonWriterCodegen(JsonCodegen codegen) {
+ JsonWriterCodegen(JsonCodegen codegen, JsonTypeResolver resolver) {
this.codegen = codegen;
+ this.resolver = resolver;
}
abstract Class> codecFieldType(JsonFieldInfo property);
@@ -77,6 +84,8 @@ abstract class JsonWriterCodegen {
abstract String writeMethod();
+ abstract String writerSlotMethod();
+
// This names private split methods in ordinary complete writers. It is not a partial-object
// capability; keep the generated literal stable so unaffected writer source remains identical.
abstract String memberGroupMethod();
@@ -85,6 +94,10 @@ abstract class JsonWriterCodegen {
abstract int splitMemberThreshold();
+ Class> memberGroupType() {
+ return null;
+ }
+
abstract PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused);
abstract void addPrefixFields(
@@ -138,8 +151,12 @@ abstract Expression utf16EnumFieldValue(
abstract Expression writeExactArray(JsonFieldInfo property, Expression value, Expression writer);
- private static boolean usesWriteCodec(JsonFieldInfo property) {
- return JsonCodegen.usesWriteCodec(property);
+ abstract boolean writesStringCollectionDirectly(JsonFieldInfo property);
+
+ private boolean usesWriteCodec(JsonFieldInfo property) {
+ return property.writeKind() == JsonFieldKind.COLLECTION
+ ? !writesStringCollectionDirectly(property)
+ : JsonCodegen.usesWriteCodec(property);
}
static Reference fieldRef(String name, Class> type) {
@@ -178,7 +195,10 @@ private static void addGeneratedConstructor(
}
String genWriterCode(
- JsonGeneratedCodecBuilder builder, Class> type, JsonFieldInfo[] properties) {
+ JsonGeneratedCodecBuilder builder,
+ Class> type,
+ JsonFieldInfo[] properties,
+ int[] groupEnds) {
ownerType = type;
CodegenContext ctx = builder.context();
ctx.addImports(writerType());
@@ -191,36 +211,41 @@ String genWriterCode(
ctx.addField(JsonFieldInfo.class, "wp" + i);
}
if (storesWriteCodec(property)) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(property)), "w" + i);
+ addWriterCodecField(ctx, property, "w" + i);
}
if (usesPrefix(property)) {
addPrefixFields(ctx, property, i, prefixFields);
}
}
- addGeneratedConstructor(
- ctx,
- writerConstructorExpression(properties, prefixFields),
- JsonFieldInfo[].class,
- "properties",
- JsonCodegen.generatedCodecArrayType(ctx, codecArrayType()),
- "codecs");
String bodyCode;
- // Keep the sole nullable capability entry small for wide POJOs. Callers can inline its null
- // ownership without absorbing the independently compiled field graph into a container loop.
- if (properties.length >= splitMemberThreshold()) {
- String objectMethod = writeMethod() + "Object";
- addGeneratedMethod(
- ctx,
- "private final",
- objectMethod,
+ // A non-null group list means the exact generated field work needs multiple compilation
+ // units. UTF-8 bodies and groups share one real invocation BCI; other representations retain
+ // their natural generated-method boundaries.
+ if (groupEnds != null) {
+ Expression body =
writeExpression(
- builder, properties, objectStartFused, new Reference("object", TypeRef.of(type))),
- void.class,
- writerType(),
- "writer",
- type,
- "object");
- bodyCode = "this." + objectMethod + "(writer, (" + ctx.type(type) + ") value);\n";
+ builder,
+ properties,
+ objectStartFused,
+ new Reference("object", TypeRef.of(type)),
+ groupEnds);
+ Class> groupType = memberGroupType();
+ if (groupType == null) {
+ String objectMethod = writeMethod() + "Object";
+ addGeneratedMethod(
+ ctx,
+ "private final",
+ objectMethod,
+ body,
+ void.class,
+ writerType(),
+ "writer",
+ type,
+ "object");
+ bodyCode = "this." + objectMethod + "(writer, (" + ctx.type(type) + ") value);\n";
+ } else {
+ bodyCode = addTrampolineBody(ctx, body, groupType);
+ }
} else {
ctx.clearExprState();
Expression castObject =
@@ -230,10 +255,17 @@ builder, properties, objectStartFused, new Reference("object", TypeRef.of(type))
Expression object =
properties.length <= 1 ? castObject : new Expression.Variable("object", castObject);
Code.ExprCode body =
- writeExpression(builder, properties, objectStartFused, object).genCode(ctx);
+ writeExpression(builder, properties, objectStartFused, object, null).genCode(ctx);
bodyCode = body.code();
bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode);
}
+ addGeneratedConstructor(
+ ctx,
+ writerConstructorExpression(properties, prefixFields),
+ JsonFieldInfo[].class,
+ "properties",
+ JsonCodegen.generatedCodecArrayType(ctx, codecArrayType()),
+ "codecs");
ctx.addMethod(
"@Override public final",
writeMethod(),
@@ -261,10 +293,10 @@ String genAnyWriterCode(
}
boolean storesAnyWriter = storesAnyWriter(any);
if (storesAnyWriter) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, completeWriterType()), "anyWriter");
+ addAnyWriterField(ctx, any);
addGeneratedConstructor(
ctx,
- anyWriterConstructorExpression(properties, prefixFields, true),
+ anyWriterConstructorExpression(properties, prefixFields, any, true),
ObjectCodec.class,
"owner",
JsonFieldInfo[].class,
@@ -276,7 +308,7 @@ String genAnyWriterCode(
} else {
addGeneratedConstructor(
ctx,
- anyWriterConstructorExpression(properties, prefixFields, false),
+ anyWriterConstructorExpression(properties, prefixFields, any, false),
ObjectCodec.class,
"owner",
JsonFieldInfo[].class,
@@ -369,10 +401,10 @@ String genUnwrappedWriterCode(
addAnyGetterMethod(ctx, type, any);
}
if (storesAnyWriter(any)) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, completeWriterType()), "anyWriter");
+ addAnyWriterField(ctx, any);
addGeneratedConstructor(
ctx,
- anyWriterConstructorExpression(leaves, prefixFields, true),
+ anyWriterConstructorExpression(leaves, prefixFields, any, true),
ObjectCodec.class,
"owner",
JsonFieldInfo[].class,
@@ -384,7 +416,7 @@ String genUnwrappedWriterCode(
} else {
addGeneratedConstructor(
ctx,
- anyWriterConstructorExpression(leaves, prefixFields, false),
+ anyWriterConstructorExpression(leaves, prefixFields, any, false),
ObjectCodec.class,
"owner",
JsonFieldInfo[].class,
@@ -718,7 +750,7 @@ private void addWriterFields(
ctx.addField(JsonFieldInfo.class, "wp" + i);
}
if (storesWriteCodec(property)) {
- ctx.addField(JsonCodegen.generatedCodecType(ctx, codecFieldType(property)), "w" + i);
+ addWriterCodecField(ctx, property, "w" + i);
}
if (usesPrefix(property)) {
addPrefixFields(ctx, property, i, prefixFields);
@@ -727,7 +759,7 @@ private void addWriterFields(
}
private Expression anyWriterConstructorExpression(
- JsonFieldInfo[] properties, PrefixFields prefixFields, boolean storesAnyWriter) {
+ JsonFieldInfo[] properties, PrefixFields prefixFields, AnyInfo any, boolean storesAnyWriter) {
Expression.ListExpression expressions =
new Expression.ListExpression(
writerConstructorExpression(properties, prefixFields),
@@ -735,16 +767,48 @@ private Expression anyWriterConstructorExpression(
new Reference("this.owner", TypeRef.of(ObjectCodec.class)),
new Reference("owner", TypeRef.of(ObjectCodec.class))));
if (storesAnyWriter) {
- expressions.add(
- new Expression.Assign(
- new Reference("this.anyWriter", TypeRef.of(completeWriterType())),
- new Reference("anyWriter", TypeRef.of(completeWriterType()))));
+ if (usesAnyWriterSlot(any)) {
+ Expression owner = new Reference("owner", TypeRef.of(ObjectCodec.class));
+ Expression anyInfo =
+ new Expression.Invoke(owner, "anyInfo", TypeRef.of(AnyInfo.class)).inline();
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.anyWriter", TypeRef.of(JsonTypeInfo.class)),
+ new Expression.Invoke(anyInfo, "valueTypeInfo", TypeRef.of(JsonTypeInfo.class))
+ .inline()));
+ } else {
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.anyWriter", TypeRef.of(completeWriterType())),
+ new Reference("anyWriter", TypeRef.of(completeWriterType()))));
+ }
}
return expressions;
}
+ private void addWriterCodecField(CodegenContext ctx, JsonFieldInfo property, String name) {
+ if (usesWriterSlot(property)) {
+ ctx.addField(true, ctx.type(JsonTypeInfo.class), name, null);
+ } else {
+ addCapabilityField(ctx, codecFieldType(property), name);
+ }
+ }
+
+ private void addAnyWriterField(CodegenContext ctx, AnyInfo any) {
+ if (usesAnyWriterSlot(any)) {
+ ctx.addField(true, ctx.type(JsonTypeInfo.class), "anyWriter", null);
+ } else {
+ addCapabilityField(ctx, completeWriterType(), "anyWriter");
+ }
+ }
+
+ private static void addCapabilityField(CodegenContext ctx, Class> type, String name) {
+ ctx.addField(true, JsonCodegen.generatedCodecType(ctx, type), name, null);
+ }
+
private boolean storesAnyWriter(AnyInfo any) {
- return !any.valueTypeInfo().usesDefaultObjectCodec() || any.valueRawType() != ownerType;
+ return resolver.canonicalObjectCodec(any.valueTypeInfo()) == null
+ || any.valueRawType() != ownerType;
}
static final class PrefixFields {
@@ -782,12 +846,20 @@ private Expression writerConstructorExpression(
new Reference("this.wp" + i, TypeRef.of(JsonFieldInfo.class)), property));
}
if (storesWriteCodec(properties[i])) {
- Class> codecType = codecFieldType(properties[i]);
- expressions.add(
- new Expression.Assign(
- new Reference("this.w" + i, TypeRef.of(codecType)),
- new Expression.Cast(
- new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType))));
+ if (usesWriterSlot(properties[i])) {
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.w" + i, TypeRef.of(JsonTypeInfo.class)),
+ new Expression.Invoke(property, "writeTypeInfo", TypeRef.of(JsonTypeInfo.class))
+ .inline()));
+ } else {
+ Class> codecType = codecFieldType(properties[i]);
+ expressions.add(
+ new Expression.Assign(
+ new Reference("this.w" + i, TypeRef.of(codecType)),
+ new Expression.Cast(
+ new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType))));
+ }
}
if (usesPrefix(properties[i])) {
addPrefixAssignments(expressions, property, properties[i], i, prefixFields);
@@ -800,7 +872,8 @@ private Expression writeExpression(
JsonGeneratedCodecBuilder builder,
JsonFieldInfo[] properties,
boolean objectStartFused,
- Expression object) {
+ Expression object,
+ int[] groupEnds) {
Reference writer = writerRef();
Expression.ListExpression expressions = new Expression.ListExpression();
expressions.add(object);
@@ -840,8 +913,8 @@ private Expression writeExpression(
}
}
boolean commaKnown = objectStartFused;
- boolean splitMembers = properties.length >= splitMemberThreshold();
- List memberGroup = splitMembers ? new ArrayList<>(MAX_MEMBERS_PER_METHOD) : null;
+ List memberGroup = groupEnds == null ? null : new ArrayList<>();
+ int memberGroupIndex = 0;
for (int i = firstProperty; i < properties.length; i++) {
Expression member;
if (objectStartFused && i == 0) {
@@ -851,28 +924,82 @@ private Expression writeExpression(
} else {
member = writeProp(builder, properties[i], i, commaKnown, index, object, writer);
}
- if (splitMembers && commaKnown) {
+ if (memberGroup != null && commaKnown) {
memberGroup.add(member);
- if (memberGroup.size() == MAX_MEMBERS_PER_METHOD) {
- addMemberGroup(builder, expressions, memberGroup, object, writer);
+ if (i + 1 == groupEnds[memberGroupIndex]) {
+ boolean rootGroup = memberGroupType() == null && memberGroupIndex == groupEnds.length - 1;
+ addWriterGroup(
+ builder, expressions, memberGroup, object, writer, memberGroupIndex, rootGroup);
+ memberGroupIndex++;
}
} else {
- if (splitMembers) {
- addMemberGroup(builder, expressions, memberGroup, object, writer);
- }
expressions.add(member);
}
if (properties[i].writeNull()) {
commaKnown = true;
}
}
- if (splitMembers) {
- addMemberGroup(builder, expressions, memberGroup, object, writer, true);
+ if (memberGroup != null) {
+ boolean directRoot =
+ memberGroupType() == null
+ && memberGroupIndex == 0
+ && memberGroup.isEmpty()
+ && groupEnds.length == 1
+ && groupEnds[0] == properties.length;
+ if (!directRoot && (memberGroupIndex != groupEnds.length || !memberGroup.isEmpty())) {
+ throw new ForyJsonException("Invalid generated writer group boundaries");
+ }
}
expressions.add(new Expression.Invoke(writer, "writeObjectEnd"));
return expressions;
}
+ private String addTrampolineBody(CodegenContext ctx, Expression body, Class> bodyType) {
+ ctx.clearExprState();
+ Code.ExprCode bodyExpr = body.genCode(ctx);
+ String bodyCode = bodyExpr.code();
+ bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode);
+ if (!bodyCode.isEmpty()) {
+ bodyCode = " " + CodeGenerator.alignIndent(bodyCode, 4) + "\n";
+ }
+
+ String fieldName = "writeBody";
+ String bodyClass = "WriteBody";
+ ctx.addImports(JsonTrampolineInvoke.class, bodyType);
+ String bodyTypeName = ctx.type(bodyType);
+ ctx.addField(true, bodyTypeName, fieldName, null);
+ // The generated receiver below owns the real object body, not a forwarding call to another
+ // generated method. It is constructed once, stored through a final interface-typed field, and
+ // invoked through JsonTrampolineInvoke's one shared invokeinterface BCI. Keeping all three
+ // properties is essential: a concrete field reveals the exact type, a per-class interface call
+ // creates separate profiles, and a forwarding receiver lets compilation order decide whether
+ // the real body is absorbed before it reaches level 4. The enclosing generated codec
+ // constructor finishes all receiver assignment before the resolver publishes that codec.
+ ctx.addInitCode(
+ "class "
+ + bodyClass
+ + " implements "
+ + bodyTypeName
+ + " {\n"
+ + " @Override public void writeUtf8("
+ + ctx.type(writerType())
+ + " writer, Object value) {\n"
+ + " "
+ + ctx.type(ownerType)
+ + " object = ("
+ + ctx.type(ownerType)
+ + ") value;\n"
+ + bodyCode
+ + " }\n"
+ + "}\n"
+ + "this."
+ + fieldName
+ + " = new "
+ + bodyClass
+ + "();");
+ return ctx.type(JsonTrampolineInvoke.class) + ".writeUtf8(" + fieldName + ", writer, value);\n";
+ }
+
private Expression writeAnyExpression(
JsonGeneratedCodecBuilder builder,
JsonFieldInfo[] properties,
@@ -985,6 +1112,13 @@ private Expression writeAny(
}
Expression map =
new Expression.Variable("anyMap", cast(inline(mapValue), TypeRef.of(Map.class)));
+ Expression anyCodec = new Reference("this", TypeRef.of(completeWriterType()));
+ if (storesAnyWriter(any)) {
+ anyCodec =
+ usesAnyWriterSlot(any)
+ ? writerFromSlot(fieldRef("anyWriter", JsonTypeInfo.class))
+ : fieldRef("anyWriter", completeWriterType());
+ }
return new Expression.ListExpression(
map,
new Expression.Invoke(
@@ -994,12 +1128,106 @@ private Expression writeAny(
false,
writerRef(),
map,
- storesAnyWriter(any)
- ? fieldRef("anyWriter", completeWriterType())
- : new Reference("this", TypeRef.of(completeWriterType())),
+ anyCodec,
written));
}
+ private void addWriterGroup(
+ JsonGeneratedCodecBuilder builder,
+ Expression.ListExpression expressions,
+ List memberGroup,
+ Expression object,
+ Reference writer,
+ int groupIndex,
+ boolean rootGroup) {
+ Class> groupType = memberGroupType();
+ if (rootGroup) {
+ for (Expression member : memberGroup) {
+ expressions.add(member);
+ }
+ memberGroup.clear();
+ return;
+ }
+ expressions.add(
+ groupType != null
+ ? trampolineGroupInvoke(builder, memberGroup, object, writer, groupIndex, groupType)
+ : memberGroupInvoke(builder, memberGroup, object, writer));
+ memberGroup.clear();
+ }
+
+ private Expression memberGroupInvoke(
+ JsonGeneratedCodecBuilder builder,
+ List memberGroup,
+ Expression object,
+ Reference writer) {
+ LinkedHashSet cutPoints = new LinkedHashSet<>();
+ cutPoints.add(object);
+ cutPoints.add(writer);
+ return ExpressionOptimizer.invokeGenerated(
+ builder.context(),
+ cutPoints,
+ new Expression.ListExpression(new ArrayList<>(memberGroup)),
+ memberGroupMethod(),
+ false);
+ }
+
+ private Expression trampolineGroupInvoke(
+ JsonGeneratedCodecBuilder builder,
+ List memberGroup,
+ Expression object,
+ Reference writer,
+ int groupIndex,
+ Class> groupType) {
+ CodegenContext ctx = builder.context();
+ ctx.clearExprState();
+ Code.ExprCode body = new Expression.ListExpression(new ArrayList<>(memberGroup)).genCode(ctx);
+ String bodyCode = body.code();
+ bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode);
+ if (!bodyCode.isEmpty()) {
+ bodyCode = " " + CodeGenerator.alignIndent(bodyCode, 4) + "\n";
+ }
+
+ String fieldName = "writeGroup" + groupIndex;
+ String groupClass = memberGroupClassName(groupIndex);
+ ctx.addImports(JsonTrampolineInvoke.class, groupType);
+ String groupTypeName = ctx.type(groupType);
+ ctx.addField(true, groupTypeName, fieldName, null);
+ // This receiver directly owns the schema field body and is stored in a final interface-typed
+ // field. Do not extract the body into a generated forwarding method: the forwarding receiver
+ // can then compile either before or after the target body reaches level 4, producing two valid
+ // but materially different C2 layouts. Do not emit invokeinterface here either; every body and
+ // group must contribute its real receiver to JsonTrampolineInvoke's one shared profile. The
+ // enclosing generated codec is published only after every final receiver field is assigned.
+ ctx.addInitCode(
+ "class "
+ + groupClass
+ + " implements "
+ + groupTypeName
+ + " {\n"
+ + " @Override public void writeUtf8("
+ + ctx.type(writerType())
+ + " writer, Object value) {\n"
+ + " "
+ + ctx.type(ownerType)
+ + " object = ("
+ + ctx.type(ownerType)
+ + ") value;\n"
+ + bodyCode
+ + " }\n"
+ + "}\n"
+ + "this."
+ + fieldName
+ + " = new "
+ + groupClass
+ + "();");
+ return new Expression.StaticInvoke(
+ JsonTrampolineInvoke.class, "writeUtf8", fieldRef(fieldName, groupType), writer, object);
+ }
+
+ static String memberGroupClassName(int groupIndex) {
+ return "WriteGroup" + groupIndex;
+ }
+
private void addMemberGroup(
JsonGeneratedCodecBuilder builder,
Expression.ListExpression expressions,
@@ -1026,19 +1254,22 @@ private void addMemberGroup(
memberGroup.clear();
return;
}
- LinkedHashSet cutPoints = new LinkedHashSet<>();
- cutPoints.add(object);
- cutPoints.add(writer);
- expressions.add(
- ExpressionOptimizer.invokeGenerated(
- builder.context(),
- cutPoints,
- new Expression.ListExpression(new ArrayList<>(memberGroup)),
- memberGroupMethod(),
- false));
+ expressions.add(memberGroupInvoke(builder, memberGroup, object, writer));
memberGroup.clear();
}
+ static int firstGroupMember(JsonFieldInfo[] properties) {
+ if (canFuseObjectStart(properties)) {
+ return 0;
+ }
+ for (int i = 0; i < properties.length; i++) {
+ if (properties[i].writeNull()) {
+ return i + 1;
+ }
+ }
+ return properties.length;
+ }
+
private static boolean canFuseObjectStart(JsonFieldInfo[] properties) {
if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) {
return false;
@@ -1084,8 +1315,7 @@ private Expression writeProp(
kind == JsonFieldKind.MAP
|| kind == JsonFieldKind.ARRAY && writeExactArray(property, value, writer) == null
|| kind == JsonFieldKind.OBJECT && writeExactScalar(property, value, writer) == null
- || kind == JsonFieldKind.COLLECTION
- && !JsonCodegen.writesStringCollectionDirectly(property);
+ || kind == JsonFieldKind.COLLECTION && !writesStringCollectionDirectly(property);
if (onlyCodec) {
return new Expression.ListExpression(
value,
@@ -1206,7 +1436,7 @@ private Expression writeValue(
case MAP:
return writeCodec(property, id, value, writer);
case COLLECTION:
- if (JsonCodegen.writesStringCollectionDirectly(property)) {
+ if (writesStringCollectionDirectly(property)) {
return writeStringCollection(value, writer);
}
return writeCodec(property, id, value, writer);
@@ -1251,20 +1481,36 @@ static long packedPrefixWord(byte[] prefix, int offset) {
private Expression writeCodec(
JsonFieldInfo property, int id, Expression value, Expression writer) {
- boolean object = property.writeTypeInfo().usesDefaultObjectCodec();
+ boolean object = resolver.canonicalObjectCodec(property.writeTypeInfo()) != null;
Expression codec =
object && property.writeRawType() == ownerType
? new Reference("this", TypeRef.of(completeWriterType()))
- : fieldRef("w" + id, codecFieldType(property));
+ : usesWriterSlot(property)
+ ? writerFromSlot(fieldRef("w" + id, JsonTypeInfo.class))
+ : fieldRef("w" + id, codecFieldType(property));
return new Expression.Invoke(codec, writeMethod(), writer, value);
}
+ private Expression writerFromSlot(Expression slot) {
+ return new Expression.Invoke(slot, writerSlotMethod(), TypeRef.of(completeWriterType()))
+ .inline();
+ }
+
private boolean storesWriteCodec(JsonFieldInfo property) {
return usesWriteCodec(property)
- && (!property.writeTypeInfo().usesDefaultObjectCodec()
+ && (resolver.canonicalObjectCodec(property.writeTypeInfo()) == null
|| property.writeRawType() != ownerType);
}
+ private boolean usesWriterSlot(JsonFieldInfo property) {
+ return storesWriteCodec(property)
+ && resolver.usesWriterSlot(ownerType, property.writeTypeInfo());
+ }
+
+ private boolean usesAnyWriterSlot(AnyInfo any) {
+ return storesAnyWriter(any) && resolver.usesWriterSlot(ownerType, any.valueTypeInfo());
+ }
+
private static Expression writeStringCollection(Expression value, Expression writer) {
return new Expression.Invoke(writer, "writeStringCollection", value);
}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java
index 10d0346e6b..25426eaf74 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java
@@ -25,16 +25,21 @@
import org.apache.fory.json.meta.JsonAsciiToken;
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.reflect.TypeRef;
final class Latin1ReaderCodegen extends JsonReaderCodegen {
- Latin1ReaderCodegen(JsonCodegen codegen) {
- super(codegen);
+ Latin1ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) {
+ super(codegen, resolver, true);
+ }
+
+ Latin1ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, int[] fastReadGroupEnds) {
+ super(codegen, resolver, true, fastReadGroupEnds);
}
@Override
Class> codecFieldType(JsonFieldInfo property) {
- return codegen.latin1ReaderFieldType(property.readTypeInfo());
+ return codegen.latin1ReaderFieldType(property.readTypeInfo(), resolver);
}
@Override
@@ -57,6 +62,11 @@ String readMethod() {
return "readLatin1";
}
+ @Override
+ String readerSlotMethod() {
+ return "latin1Reader";
+ }
+
@Override
String readEnumMethod(boolean tokenValueRead, boolean hashFallback) {
return tokenValueRead
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
index 732c8d9433..9edb6301cd 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java
@@ -25,19 +25,20 @@
import org.apache.fory.json.ForyJsonException;
import org.apache.fory.json.codec.StringWriterCodec;
import org.apache.fory.json.meta.JsonFieldInfo;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.json.writer.StringJsonWriter;
import org.apache.fory.reflect.TypeRef;
final class StringWriterCodegen extends JsonWriterCodegen {
private static final int MIN_SPLIT_MEMBERS = 10;
- StringWriterCodegen(JsonCodegen codegen) {
- super(codegen);
+ StringWriterCodegen(JsonCodegen codegen, JsonTypeResolver resolver) {
+ super(codegen, resolver);
}
@Override
Class> codecFieldType(JsonFieldInfo property) {
- return codegen.stringWriterFieldType(property.writeTypeInfo());
+ return codegen.stringWriterFieldType(property.writeTypeInfo(), resolver);
}
@Override
@@ -60,6 +61,11 @@ String writeMethod() {
return "writeString";
}
+ @Override
+ String writerSlotMethod() {
+ return "stringWriter";
+ }
+
@Override
String memberGroupMethod() {
return "writeStringMembers";
@@ -75,6 +81,11 @@ int splitMemberThreshold() {
return MIN_SPLIT_MEMBERS;
}
+ @Override
+ boolean writesStringCollectionDirectly(JsonFieldInfo property) {
+ return JsonCodegen.writesStringCollectionDirectly(property);
+ }
+
@Override
PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused) {
PrefixFields fields = new PrefixFields(properties.length);
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java
index b0588bf4a5..8dfa82985d 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java
@@ -24,16 +24,21 @@
import org.apache.fory.json.codec.Utf16ReaderCodec;
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.reader.Utf16JsonReader;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.reflect.TypeRef;
final class Utf16ReaderCodegen extends JsonReaderCodegen {
- Utf16ReaderCodegen(JsonCodegen codegen) {
- super(codegen);
+ Utf16ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver) {
+ super(codegen, resolver, true);
+ }
+
+ Utf16ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, int[] fastReadGroupEnds) {
+ super(codegen, resolver, true, fastReadGroupEnds);
}
@Override
Class> codecFieldType(JsonFieldInfo property) {
- return codegen.utf16ReaderFieldType(property.readTypeInfo());
+ return codegen.utf16ReaderFieldType(property.readTypeInfo(), resolver);
}
@Override
@@ -56,6 +61,11 @@ String readMethod() {
return "readUtf16";
}
+ @Override
+ String readerSlotMethod() {
+ return "utf16Reader";
+ }
+
@Override
String readEnumMethod(boolean tokenValueRead, boolean hashFallback) {
return "readNextUtf16Enum";
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java
new file mode 100644
index 0000000000..b61b1eb26b
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionReaderCodegen.java
@@ -0,0 +1,386 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.codegen;
+
+import java.util.ArrayList;
+import org.apache.fory.codegen.Code;
+import org.apache.fory.codegen.CodegenContext;
+import org.apache.fory.json.codec.Utf8ReaderCodec;
+import org.apache.fory.json.reader.Utf8JsonReader;
+
+/** Generates one exact declared ArrayList-backed UTF-8 collection capability. */
+final class Utf8CollectionReaderCodegen {
+ // The ninth value remains scalar until the following separator proves whether the list ends or
+ // continues. Exact size nine therefore allocates nine slots, while a continuation starts at the
+ // same capacity 13 that ArrayList growth would have selected, without creating and copying a
+ // discarded nine-slot array.
+ private static final int ARRAY_LIST_PREFIX_SIZE = 9;
+ private static final int ARRAY_LIST_FIRST_GROWTH =
+ ARRAY_LIST_PREFIX_SIZE + (ARRAY_LIST_PREFIX_SIZE >> 1);
+
+ String genCode(String generatedPackage, String className, boolean stringElements) {
+ CodegenContext ctx = new CodegenContext();
+ ctx.setPackage(generatedPackage);
+ ctx.setClassName(className);
+ ctx.setClassModifiers("final");
+ ctx.addImports(ArrayList.class, Utf8JsonReader.class, Utf8ReaderCodec.class);
+ ctx.implementsInterfaces(ctx.type(Utf8ReaderCodec.class));
+ ctx.addField(true, ctx.type(Utf8ReaderCodec.class), "elementReader", null);
+ ctx.addConstructor(
+ "this.elementReader = elementReader;", Utf8ReaderCodec.class, "elementReader");
+ if (stringElements) {
+ addStringArrayListMethods(ctx);
+ } else {
+ ctx.addMethod(
+ "@Override public final",
+ "readUtf8",
+ readBody(),
+ Object.class,
+ Utf8JsonReader.class,
+ "reader");
+ }
+ return ctx.genCode();
+ }
+
+ private static void addStringArrayListMethods(CodegenContext ctx) {
+ ctx.addMethod(
+ "@Override public final",
+ "readUtf8",
+ readStringArrayListCode(ctx),
+ Object.class,
+ Utf8JsonReader.class,
+ "reader");
+ ctx.addMethod(
+ "private final",
+ "readArrayListBody",
+ readStringArrayListBodyCode(ctx),
+ ArrayList.class,
+ Utf8JsonReader.class,
+ "reader",
+ String.class,
+ "e0",
+ String.class,
+ "e1");
+ ctx.addMethod(
+ "private final",
+ "readArrayListTail",
+ readStringArrayListTailCode(ctx),
+ ArrayList.class,
+ Utf8JsonReader.class,
+ "reader",
+ String.class,
+ "e0",
+ String.class,
+ "e1",
+ String.class,
+ "e2",
+ String.class,
+ "e3");
+ ctx.addMethod(
+ "private final",
+ "readArrayListLongTail",
+ readStringArrayListLongTailCode(ctx),
+ ArrayList.class,
+ Utf8JsonReader.class,
+ "reader",
+ String.class,
+ "e0",
+ String.class,
+ "e1",
+ String.class,
+ "e2",
+ String.class,
+ "e3",
+ String.class,
+ "e4",
+ String.class,
+ "e5");
+ ctx.addMethod(
+ "private final",
+ "readArrayListLoop",
+ readStringArrayListLoopCode(ctx),
+ ArrayList.class,
+ Utf8JsonReader.class,
+ "reader",
+ String.class,
+ "e0",
+ String.class,
+ "e1",
+ String.class,
+ "e2",
+ String.class,
+ "e3",
+ String.class,
+ "e4",
+ String.class,
+ "e5",
+ String.class,
+ "e6",
+ String.class,
+ "e7");
+ }
+
+ private static String readStringArrayListCode(CodegenContext ctx) {
+ return "if (reader.tryReadNullToken()) {\n"
+ + " return null;\n"
+ + "}\n"
+ + "reader.enterDepth();\n"
+ + "reader.expectNextToken('[');\n"
+ + "if (reader.consumeNextToken(']')) {\n"
+ + " reader.exitDepth();\n"
+ + " return new ArrayList(0);\n"
+ + "}\n"
+ + readStringElement(ctx, "e0", "E0")
+ + "int nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(1);\n"
+ + " list.add(e0);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e1", "E1")
+ + "return readArrayListBody(reader, e0, e1);";
+ }
+
+ private static String readStringArrayListBodyCode(CodegenContext ctx) {
+ // Each prefix method consumes its incoming separator state locally. This keeps cursor
+ // publication and the following quote probe in one generated compilation unit.
+ return "int nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(2);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e2", "B2")
+ + "nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(3);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e3", "B3")
+ + "return readArrayListTail(reader, e0, e1, e2, e3);";
+ }
+
+ private static String readStringArrayListTailCode(CodegenContext ctx) {
+ return "int nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(4);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " list.add(e3);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e4", "T4")
+ + "nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(5);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " list.add(e3);\n"
+ + " list.add(e4);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e5", "T5")
+ + "return readArrayListLongTail(reader, e0, e1, e2, e3, e4, e5);";
+ }
+
+ private static String readStringArrayListLongTailCode(CodegenContext ctx) {
+ return "int nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(6);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " list.add(e3);\n"
+ + " list.add(e4);\n"
+ + " list.add(e5);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e6", "L6")
+ + "nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(7);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " list.add(e3);\n"
+ + " list.add(e4);\n"
+ + " list.add(e5);\n"
+ + " list.add(e6);\n"
+ + " return list;\n"
+ + "}\n"
+ + readStringArrayElement(ctx, "e7", "L7")
+ + "if (!reader.consumeNextCommaOrEndArray()) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList(8);\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " list.add(e3);\n"
+ + " list.add(e4);\n"
+ + " list.add(e5);\n"
+ + " list.add(e6);\n"
+ + " list.add(e7);\n"
+ + " return list;\n"
+ + "}\n"
+ + "return readArrayListLoop(reader, e0, e1, e2, e3, e4, e5, e6, e7);";
+ }
+
+ private static String readStringArrayListLoopCode(CodegenContext ctx) {
+ return "String e8 = (String) elementReader.readUtf8(reader);\n"
+ + "int nextElement = reader.consumeNextStringArrayElement();\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_END) {\n"
+ + " reader.exitDepth();\n"
+ + " ArrayList list = new ArrayList("
+ + ARRAY_LIST_PREFIX_SIZE
+ + ");\n"
+ + " list.add(e0);\n"
+ + " list.add(e1);\n"
+ + " list.add(e2);\n"
+ + " list.add(e3);\n"
+ + " list.add(e4);\n"
+ + " list.add(e5);\n"
+ + " list.add(e6);\n"
+ + " list.add(e7);\n"
+ + " list.add(e8);\n"
+ + " return list;\n"
+ + "}\n"
+ + "ArrayList list = new ArrayList("
+ + ARRAY_LIST_FIRST_GROWTH
+ + ");\n"
+ + "list.add(e0);\n"
+ + "list.add(e1);\n"
+ + "list.add(e2);\n"
+ + "list.add(e3);\n"
+ + "list.add(e4);\n"
+ + "list.add(e5);\n"
+ + "list.add(e6);\n"
+ + "list.add(e7);\n"
+ + "list.add(e8);\n"
+ + "do {\n"
+ + " String element;\n"
+ + " if (nextElement == Utf8JsonReader.STRING_ARRAY_QUOTED) {\n"
+ + readQuotedStringElement(ctx, "quotedElement", "LoopQuoted")
+ + " element = quotedElement;\n"
+ + " } else {\n"
+ + " element = (String) elementReader.readUtf8(reader);\n"
+ + " }\n"
+ + " list.add(element);\n"
+ + " nextElement = reader.consumeNextStringArrayElement();\n"
+ + "} while (nextElement != Utf8JsonReader.STRING_ARRAY_END);\n"
+ + "reader.exitDepth();\n"
+ + "return list;";
+ }
+
+ private static String readStringElement(CodegenContext ctx, String variable, String id) {
+ ctx.clearExprState();
+ Code.ExprCode expression = Utf8ReaderCodegen.readStringElement(id).genCode(ctx);
+ String expressionCode = expression.code();
+ return (expressionCode == null ? "" : expressionCode + "\n")
+ + "String "
+ + variable
+ + " = "
+ + expression.value()
+ + ";\n";
+ }
+
+ private static String readStringArrayElement(CodegenContext ctx, String variable, String id) {
+ String quoted = variable + "Quoted";
+ return "String "
+ + variable
+ + ";\n"
+ + "if (nextElement == Utf8JsonReader.STRING_ARRAY_QUOTED) {\n"
+ + readQuotedStringElement(ctx, quoted, id)
+ + " "
+ + variable
+ + " = "
+ + quoted
+ + ";\n"
+ + "} else {\n"
+ + " "
+ + variable
+ + " = (String) elementReader.readUtf8(reader);\n"
+ + "}\n";
+ }
+
+ private static String readQuotedStringElement(CodegenContext ctx, String variable, String id) {
+ ctx.clearExprState();
+ Code.ExprCode expression = Utf8ReaderCodegen.readQuotedStringElement(id).genCode(ctx);
+ String expressionCode = expression.code();
+ return (expressionCode == null ? "" : expressionCode + "\n")
+ + "String "
+ + variable
+ + " = "
+ + expression.value()
+ + ";\n";
+ }
+
+ private static String readBody() {
+ StringBuilder code = new StringBuilder();
+ code.append("if (reader.tryReadNullToken()) {\n return null;\n}\n");
+ code.append("reader.enterDepth();\n");
+ code.append("reader.expectNextToken('[');\n");
+ code.append("if (reader.consumeNextToken(']')) {\n");
+ code.append(" reader.exitDepth();\n return new ArrayList(0);\n}\n");
+ for (int i = 0; i < 8; i++) {
+ code.append("Object e").append(i).append(" = null;\n");
+ }
+ code.append("ArrayList list = null;\nint size = 0;\n");
+ code.append("do {\n");
+ code.append(" Object element = elementReader.readUtf8(reader);\n");
+ code.append(" if (list == null) {\n switch (size) {\n");
+ for (int i = 0; i < 8; i++) {
+ code.append(" case ").append(i).append(": e").append(i).append(" = element; break;\n");
+ }
+ code.append(" default:\n list = new ArrayList(9);\n");
+ for (int i = 0; i < 8; i++) {
+ code.append(" list.add(e").append(i).append(");\n");
+ }
+ code.append(" list.add(element);\n }\n");
+ code.append(" } else {\n list.add(element);\n }\n");
+ code.append(" size++;\n} while (reader.consumeNextCommaOrEndArray());\n");
+ code.append("reader.exitDepth();\n");
+ code.append("if (list != null) {\n return list;\n}\n");
+ code.append("list = new ArrayList(size);\n");
+ code.append("switch (size) {\n");
+ for (int size = 1; size <= 8; size++) {
+ code.append(" case ").append(size).append(":\n");
+ for (int i = 0; i < size; i++) {
+ code.append(" list.add(e").append(i).append(");\n");
+ }
+ code.append(" break;\n");
+ }
+ code.append(" default: throw new IllegalStateException();\n}\n");
+ code.append("return list;\n");
+ return code.toString();
+ }
+}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java
new file mode 100644
index 0000000000..89da739dc9
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8CollectionWriterCodegen.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.codegen;
+
+import java.util.ArrayList;
+import org.apache.fory.codegen.CodegenContext;
+import org.apache.fory.json.codec.Utf8WriterCodec;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+
+/** Generates one exact declared UTF-8 collection capability. */
+final class Utf8CollectionWriterCodegen {
+ String genCode(String generatedPackage, String className, boolean stringElements) {
+ CodegenContext ctx = new CodegenContext();
+ ctx.setPackage(generatedPackage);
+ ctx.setClassName(className);
+ ctx.setClassModifiers("final");
+ ctx.addImports(ArrayList.class, Utf8JsonWriter.class, Utf8WriterCodec.class);
+ ctx.implementsInterfaces(ctx.type(Utf8WriterCodec.class));
+ ctx.addField(true, ctx.type(Utf8WriterCodec.class), "fallback", null);
+ if (stringElements) {
+ ctx.addConstructor("this.fallback = fallback;", Utf8WriterCodec.class, "fallback");
+ } else {
+ ctx.addField(true, ctx.type(Utf8WriterCodec.class), "elementWriter", null);
+ ctx.addConstructor(
+ "this.fallback = fallback;\nthis.elementWriter = elementWriter;",
+ Utf8WriterCodec.class,
+ "fallback",
+ Utf8WriterCodec.class,
+ "elementWriter");
+ }
+ ctx.addMethod(
+ "@Override public final",
+ "writeUtf8",
+ writeBody(stringElements),
+ void.class,
+ Utf8JsonWriter.class,
+ "writer",
+ Object.class,
+ "value");
+ return ctx.genCode();
+ }
+
+ private static String writeBody(boolean stringElements) {
+ // The collection owns iteration and calls the final element codec's ordinary writeUtf8 entry.
+ // That entry owns null handling and, for a qualifying generated object, reaches the shared
+ // object-body trampoline itself. Do not call JsonTrampolineInvoke from this loop: collection
+ // cardinality would then dominate the body/group receiver profile and the element entry would
+ // no longer be the owner of its complete value semantics.
+ String elementWrite =
+ stringElements
+ ? "String element = (String) list.get(index);\n"
+ + " writer.writeComma(index);\n"
+ + " if (element == null) {\n"
+ + " writer.writeNull();\n"
+ + " } else {\n"
+ + " writer.writeString(element);\n"
+ + " }"
+ : "writer.writeComma(index);\n" + " elementWriter.writeUtf8(writer, list.get(index));";
+ return "if (value == null) {\n"
+ + " writer.writeNull();\n"
+ + " return;\n"
+ + "}\n"
+ + "if (value.getClass() != ArrayList.class) {\n"
+ + " fallback.writeUtf8(writer, value);\n"
+ + " return;\n"
+ + "}\n"
+ + "ArrayList list = (ArrayList) value;\n"
+ + "writer.writeArrayStart();\n"
+ + "for (int index = 0, size = list.size(); index < size; index++) {\n"
+ + " "
+ + elementWrite
+ + "\n"
+ + "}\n"
+ + "writer.writeArrayEnd();";
+ }
+}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java
index ce01fa5810..6d0de5f4fb 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java
@@ -21,20 +21,35 @@
import org.apache.fory.codegen.Expression;
import org.apache.fory.codegen.Expression.Reference;
+import org.apache.fory.codegen.ExpressionUtils;
import org.apache.fory.json.codec.Utf8ReaderCodec;
import org.apache.fory.json.meta.JsonAsciiToken;
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.reader.Utf8JsonReader;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.reflect.TypeRef;
final class Utf8ReaderCodegen extends JsonReaderCodegen {
- Utf8ReaderCodegen(JsonCodegen codegen) {
- super(codegen);
+ // Generated String collections own three bounded word probes. Longer inputs continue in the
+ // reader-owned scanner, keeping escape, Unicode, malformed-input, and arbitrary-length work out
+ // of each generated prefix method.
+ private static final int DIRECT_STRING_WORDS = 3;
+
+ Utf8ReaderCodegen(JsonCodegen codegen, JsonTypeResolver resolver, boolean finalDependencies) {
+ super(codegen, resolver, finalDependencies);
+ }
+
+ Utf8ReaderCodegen(
+ JsonCodegen codegen,
+ JsonTypeResolver resolver,
+ boolean finalDependencies,
+ int[] fastReadGroupEnds) {
+ super(codegen, resolver, finalDependencies, fastReadGroupEnds);
}
@Override
Class> codecFieldType(JsonFieldInfo property) {
- return codegen.utf8ReaderFieldType(property.readTypeInfo());
+ return codegen.utf8ReaderFieldType(property.readTypeInfo(), resolver, finalDependencies());
}
@Override
@@ -57,6 +72,11 @@ String readMethod() {
return "readUtf8";
}
+ @Override
+ String readerSlotMethod() {
+ return "utf8Reader";
+ }
+
@Override
String readEnumMethod(boolean tokenValueRead, boolean hashFallback) {
return tokenValueRead
@@ -74,6 +94,109 @@ String readFieldMethod() {
return "readUtf8";
}
+ static Expression readStringElement(String id) {
+ Reference reader = new Reference("reader", TypeRef.of(Utf8JsonReader.class));
+ Expression.Variable value =
+ new Expression.Variable("stringElement" + id, TypeRef.of(String.class));
+ Expression fallback =
+ new Expression.If(
+ new Expression.Invoke(reader, "tryReadNextNullToken", TypeRef.of(boolean.class))
+ .inline(),
+ new Expression.Assign(value, ExpressionUtils.nullValue(String.class)),
+ new Expression.Assign(
+ value,
+ new Expression.Invoke(
+ reader, "readNullableStringToken", TypeRef.of(String.class), true)
+ .inline()));
+ return new Expression.ListExpression(
+ value,
+ new Expression.If(
+ tryConsumeStringQuote(reader),
+ new Expression.Assign(value, readStringToken(id, reader)),
+ fallback),
+ value);
+ }
+
+ static Expression readQuotedStringElement(String id) {
+ Reference reader = new Reference("reader", TypeRef.of(Utf8JsonReader.class));
+ return readStringToken(id, reader);
+ }
+
+ private static Expression readStringToken(String id, Expression reader) {
+ Expression start =
+ new Expression.Variable(
+ "stringStart" + id,
+ new Expression.Invoke(reader, "position", TypeRef.of(int.class)).inline());
+ Expression offset = new Expression.Variable("stringOffset" + id, start);
+ Expression inputLength =
+ new Expression.Variable(
+ "stringInputLength" + id,
+ new Expression.Invoke(reader, "inputLength", TypeRef.of(int.class)).inline());
+ Expression directWordEnd =
+ new Expression.Variable(
+ "stringDirectWordEnd" + id,
+ new Expression.Subtract(
+ true, inputLength, Expression.Literal.ofInt(Long.BYTES * DIRECT_STRING_WORDS)));
+ Expression state = new Expression.Variable("stringState" + id, Expression.Literal.ofLong(0L));
+ Expression value = new Expression.Variable("stringValue" + id, TypeRef.of(String.class));
+ Expression zero = Expression.Literal.ofLong(0L);
+ Expression directScans = new Expression.Empty();
+ for (int i = 0; i < DIRECT_STRING_WORDS; i++) {
+ directScans =
+ new Expression.ListExpression(
+ new Expression.Assign(state, scanStringWord(reader, offset)),
+ new Expression.If(
+ equal(state, zero),
+ new Expression.ListExpression(advanceStringOffset(offset), directScans)));
+ }
+ directScans =
+ new Expression.If(
+ new Expression.Comparator("<=", offset, directWordEnd, true), directScans);
+ Expression finish =
+ new Expression.If(
+ notEqual(state, zero),
+ new Expression.Assign(value, finishStringWord(reader, start, offset, state)),
+ new Expression.Assign(value, readStringLongTail(reader, start, offset)));
+ return new Expression.ListExpression(
+ start, offset, inputLength, directWordEnd, state, value, directScans, finish, value);
+ }
+
+ private static Expression tryConsumeStringQuote(Expression reader) {
+ return new Expression.Invoke(reader, "tryConsumeStringQuote", TypeRef.of(boolean.class))
+ .inline();
+ }
+
+ private static Expression readStringLongTail(
+ Expression reader, Expression start, Expression offset) {
+ return new Expression.Invoke(
+ reader, "readStringTokenLongTail", TypeRef.of(String.class), start, offset)
+ .inline();
+ }
+
+ private static Expression finishStringWord(
+ Expression reader, Expression start, Expression offset, Expression state) {
+ return new Expression.Invoke(
+ reader, "finishStringWord", TypeRef.of(String.class), start, offset, state)
+ .inline();
+ }
+
+ private static Expression advanceStringOffset(Expression offset) {
+ return new Expression.Assign(
+ offset, new Expression.Add(true, offset, Expression.Literal.ofInt(Long.BYTES)));
+ }
+
+ private static Expression equal(Expression left, Expression right) {
+ return new Expression.Comparator("==", left, right, true);
+ }
+
+ private static Expression notEqual(Expression left, Expression right) {
+ return new Expression.Comparator("!=", left, right, true);
+ }
+
+ private static Expression scanStringWord(Expression reader, Expression offset) {
+ return new Expression.Invoke(reader, "scanStringWord", TypeRef.of(long.class), offset).inline();
+ }
+
@Override
boolean isDirectName(String name, boolean tokenValueRead) {
return JsonAsciiToken.isLongPackable(fieldNameToken(name));
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
index ab0ccb84ff..89b2cc600b 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java
@@ -34,19 +34,20 @@
import org.apache.fory.json.codec.Utf8WriterCodec;
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldKind;
+import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.json.writer.Utf8JsonWriter;
import org.apache.fory.reflect.TypeRef;
final class Utf8WriterCodegen extends JsonWriterCodegen {
private static final int MIN_SPLIT_MEMBERS = 12;
- Utf8WriterCodegen(JsonCodegen codegen) {
- super(codegen);
+ Utf8WriterCodegen(JsonCodegen codegen, JsonTypeResolver resolver) {
+ super(codegen, resolver);
}
@Override
Class> codecFieldType(JsonFieldInfo property) {
- return codegen.utf8WriterFieldType(property.writeTypeInfo());
+ return codegen.utf8WriterFieldType(property.writeTypeInfo(), resolver);
}
@Override
@@ -69,11 +70,24 @@ String writeMethod() {
return "writeUtf8";
}
+ @Override
+ String writerSlotMethod() {
+ return "utf8Writer";
+ }
+
@Override
String memberGroupMethod() {
return "writeUtf8Members";
}
+ @Override
+ Class> memberGroupType() {
+ // Returning the interface type opts only generated UTF-8 object bodies and member groups into
+ // JsonTrampolineInvoke's shared callsite. Collection loops keep their ordinary element-codec
+ // call, and other writer representations keep their own measured method layout.
+ return Utf8WriterCodec.class;
+ }
+
@Override
String writeAnyMethod() {
return "writeUtf8Any";
@@ -84,6 +98,12 @@ int splitMemberThreshold() {
return MIN_SPLIT_MEMBERS;
}
+ @Override
+ boolean writesStringCollectionDirectly(JsonFieldInfo property) {
+ return JsonCodegen.writesStringCollectionDirectly(property)
+ && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) == null;
+ }
+
@Override
PrefixFields prefixFields(JsonFieldInfo[] properties, boolean objectStartFused) {
PrefixFields fields = new PrefixFields(properties.length);
@@ -101,10 +121,7 @@ && canPackObjectStartString(property)) {
fields.name[i] = true;
}
} else if (!commaKnown) {
- if (property.writesRawString()
- || !canUsePackedDynamicPrefix(property)
- || !canPackSinglePrefix(property, false)
- || !canPackSinglePrefix(property, true)) {
+ if (!canPackPrefix(property, false) || !canPackPrefix(property, true)) {
fields.name[i] = true;
fields.comma[i] = true;
}
@@ -119,22 +136,6 @@ && canPackObjectStartString(property)) {
return fields;
}
- private boolean canUsePackedDynamicPrefix(JsonFieldInfo property) {
- if (property.writeNull() && !property.writeRawType().isPrimitive()) {
- return false;
- }
- switch (property.writeKind()) {
- case BYTE:
- case SHORT:
- case INT:
- case LONG:
- case STRING:
- return true;
- default:
- return false;
- }
- }
-
@Override
void addPrefixFields(CodegenContext ctx, JsonFieldInfo property, int id, PrefixFields fields) {
if (fields.name[id]) {
@@ -202,8 +203,10 @@ Expression tryWriteObjectStartString(
if (!canPackObjectStartString(property)) {
return null;
}
- return new Expression.Invoke(
- writer, "writeObjectStartWithStringField", objectPackedPrefixArgs(property, value));
+ return new Expression.ListExpression(
+ new Expression.Invoke(writer, "writeObjectStart"),
+ new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, false)),
+ new Expression.Invoke(writer, "writeString", value));
}
private static boolean canPackObjectStartString(JsonFieldInfo property) {
@@ -228,7 +231,7 @@ Expression writeNumberField(
}
return new Expression.Invoke(writer, method, utf8PrefixRef(true, id), value);
}
- if (canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) {
+ if (canPackPrefix(property, false) && canPackPrefix(property, true)) {
return new Expression.ListExpression(
new Expression.Invoke(writer, method, packedDynamicPrefixArgs(property, index, value)),
increment(index));
@@ -249,30 +252,9 @@ Expression writeStringField(
boolean commaKnown,
Expression index,
Expression writer) {
- if (commaKnown) {
- if (canPackPrefix(property, true)) {
- return new Expression.Invoke(
- writer, "writeStringField", packedPrefixArgs(property, true, value));
- }
- return new Expression.Invoke(writer, "writeStringField", utf8PrefixRef(true, id), value);
- }
- if (canPackSinglePrefix(property, false) && canPackSinglePrefix(property, true)) {
- return new Expression.ListExpression(
- new Expression.Invoke(
- writer, "writeStringField", packedDynamicPrefixArgs(property, index, value)),
- increment(index));
- }
- Expression.ListExpression expressions =
- new Expression.ListExpression(
- new Expression.Invoke(
- writer,
- "writeStringField",
- utf8PrefixRef(false, id),
- utf8PrefixRef(true, id),
- index,
- value));
- expressions.add(increment(index));
- return expressions;
+ return new Expression.ListExpression(
+ writeFieldName(property, id, commaKnown, index, writer),
+ new Expression.Invoke(writer, "writeString", value));
}
@Override
@@ -281,6 +263,11 @@ Expression writeFieldName(
if (commaKnown && canPackPrefix(property, true)) {
return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true));
}
+ if (!commaKnown && canPackPrefix(property, false) && canPackPrefix(property, true)) {
+ return new Expression.ListExpression(
+ new Expression.Invoke(writer, "writeRawValue", packedDynamicPrefixArgs(property, index)),
+ increment(index));
+ }
Expression prefix =
commaKnown
? utf8PrefixRef(true, id)
@@ -361,30 +348,19 @@ private static Expression[] packedPrefixArgs(
return args;
}
- private static Expression[] objectPackedPrefixArgs(JsonFieldInfo property, Expression value) {
- byte[] namePrefix = property.utf8NamePrefix();
- byte[] prefix = new byte[namePrefix.length + 1];
- prefix[0] = '{';
- System.arraycopy(namePrefix, 0, prefix, 1, namePrefix.length);
- return new Expression[] {
- Expression.Literal.ofLong(packedPrefixWord(prefix, 0)),
- Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES)),
- Expression.Literal.ofInt(prefix.length),
- value
- };
- }
-
private static Expression[] packedDynamicPrefixArgs(
JsonFieldInfo property, Expression index, Expression... extraArgs) {
byte[] namePrefix = property.utf8NamePrefix();
byte[] commaPrefix = property.utf8CommaNamePrefix();
- Expression[] args = new Expression[5 + extraArgs.length];
+ Expression[] args = new Expression[7 + extraArgs.length];
args[0] = Expression.Literal.ofLong(packedPrefixWord(namePrefix, 0));
- args[1] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, 0));
- args[2] = Expression.Literal.ofInt(namePrefix.length);
- args[3] = Expression.Literal.ofInt(commaPrefix.length);
- args[4] = index;
- System.arraycopy(extraArgs, 0, args, 5, extraArgs.length);
+ args[1] = Expression.Literal.ofLong(packedPrefixWord(namePrefix, Long.BYTES));
+ args[2] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, 0));
+ args[3] = Expression.Literal.ofLong(packedPrefixWord(commaPrefix, Long.BYTES));
+ args[4] = Expression.Literal.ofInt(namePrefix.length);
+ args[5] = Expression.Literal.ofInt(commaPrefix.length);
+ args[6] = index;
+ System.arraycopy(extraArgs, 0, args, 7, extraArgs.length);
return args;
}
@@ -392,9 +368,4 @@ private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) {
int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length;
return length <= Long.BYTES * 2;
}
-
- private static boolean canPackSinglePrefix(JsonFieldInfo property, boolean comma) {
- int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length;
- return length <= Long.BYTES;
- }
}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
index 00b2c73903..176db70e7f 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
@@ -52,9 +52,10 @@
*
* {@link #resolveTypes(JsonTypeResolver)} installs the resolved read and write {@link
* JsonTypeInfo} bindings after recursive object metadata has been published. Those bindings are the
- * only mutable lifecycle phase; generated instances capture the current concrete child capability
- * under the resolver-local JIT lock and receive later child replacements through resolver
- * callbacks.
+ * only mutable lifecycle phase. Generated classes are compiled independently from this stable
+ * metadata; generated instances are constructed bottom-up with final direct child capabilities. A
+ * canonical multi-object cycle instead captures the final child type-info slot owner. The complete
+ * graph is published under the resolver-local JIT lock.
*/
public final class JsonFieldInfo {
private static final int KIND_BOOLEAN = 1;
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java
new file mode 100644
index 0000000000..d66b3ea6a8
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalMath.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.reader;
+
+/** Exact integer arithmetic used by decimal readers. */
+final class DecimalMath {
+ private DecimalMath() {}
+
+ static long unsignedMultiplyHigh(long x, long y) {
+ long xlo = x & 0xffff_ffffL;
+ long xhi = x >>> 32;
+ long ylo = y & 0xffff_ffffL;
+ long yhi = y >>> 32;
+ long lowProduct = xlo * ylo;
+ long carryProduct = xhi * ylo + (lowProduct >>> 32);
+ long middle = (carryProduct & 0xffff_ffffL) + xlo * yhi;
+ return xhi * yhi + (carryProduct >>> 32) + (middle >>> 32);
+ }
+}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
index 387b226a0b..516c7c2682 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
@@ -98,6 +98,27 @@ public abstract class JsonReader {
private static final long DOUBLE_FRACTION_MASK = (1L << DOUBLE_FRACTION_BITS) - 1;
private static final long DOUBLE_INFINITY_BITS = 0x7ff0_0000_0000_0000L;
private static final long DOUBLE_MAX_FINITE_BITS = 0x7fef_ffff_ffff_ffffL;
+ private static final long[] COMPACT_DOUBLE_MANTISSAS = {
+ 0x8000_0000_0000_0000L,
+ 0xcccc_cccc_cccc_ccccL,
+ 0xa3d7_0a3d_70a3_d70aL,
+ 0x8312_6e97_8d4f_df3bL,
+ 0xd1b7_1758_e219_652bL,
+ 0xa7c5_ac47_1b47_8423L,
+ 0x8637_bd05_af6c_69b5L,
+ 0xd6bf_94d5_e57a_42bcL,
+ 0xabcc_7711_8461_cefcL,
+ 0x8970_5f41_36b4_a597L,
+ 0xdbe6_fece_bded_d5beL,
+ 0xafeb_ff0b_cb24_aafeL,
+ 0x8cbc_cc09_6f50_88cbL,
+ 0xe12e_1342_4bb4_0e13L,
+ 0xb424_dc35_095c_d80fL,
+ 0x901d_7cf7_3ab0_acd9L,
+ 0xe695_94be_c44d_e15bL,
+ 0xb877_aa32_36a4_b449L,
+ 0x9392_ee8e_921d_5d07L
+ };
private static final int DECIMAL_BOUNDARY_DIGITS = 768;
private static final double[] DOUBLE_POWERS_OF_TEN = {
1.0d,
@@ -1176,15 +1197,46 @@ protected static double compactDoubleValue(boolean negative, long unscaled, int
if (unscaled == 0) {
return negative ? -0.0d : 0.0d;
}
- long divisor = LONG_POWERS_OF_TEN[scale];
- double estimate = (double) unscaled / (double) divisor;
- long bits = correctCompactDouble(unscaled, divisor, Double.doubleToRawLongBits(estimate));
+ long bits = tryCompactDoubleBits(unscaled, scale);
+ if (bits == 0) {
+ long divisor = LONG_POWERS_OF_TEN[scale];
+ double estimate = (double) unscaled / (double) divisor;
+ bits = correctCompactDouble(unscaled, divisor, Double.doubleToRawLongBits(estimate));
+ }
if (negative) {
bits |= DOUBLE_SIGN_BIT;
}
return Double.longBitsToDouble(bits);
}
+ // The caller supplies a nonzero positive long and scale 0..18. A zero result is therefore not a
+ // value; it asks the caller to use the exact midpoint path when the high product cannot prove the
+ // rounding direction. The common path is the Eisel-Lemire decimal conversion specialized to
+ // this bounded compact domain.
+ private static long tryCompactDoubleBits(long unscaled, int scale) {
+ int leadingZeros = Long.numberOfLeadingZeros(unscaled);
+ long upper =
+ DecimalMath.unsignedMultiplyHigh(unscaled << leadingZeros, COMPACT_DOUBLE_MANTISSAS[scale]);
+ int upperBit = (int) (upper >>> 63);
+ long mantissa = upper >>> (upperBit + 9);
+ leadingZeros += 1 ^ upperBit;
+ long roundBits = upper & 0x1ff;
+ if (roundBits == 0x1ff || (roundBits == 0 && (mantissa & 3) == 1)) {
+ return 0;
+ }
+ mantissa = (mantissa + 1) >>> 1;
+ if (mantissa >= (1L << (DOUBLE_FRACTION_BITS + 1))) {
+ mantissa = 1L << DOUBLE_FRACTION_BITS;
+ leadingZeros--;
+ }
+ mantissa &= DOUBLE_FRACTION_MASK;
+ long exponent = ((217706L * -scale) >> 16) + 1023 + 64 - leadingZeros;
+ if (exponent < 1 || exponent > 2046) {
+ return 0;
+ }
+ return mantissa | (exponent << DOUBLE_FRACTION_BITS);
+ }
+
private static long correctCompactDouble(long unscaled, long divisor, long bits) {
// Rounded long operands plus one hardware division stay close to the exact decimal rational.
// Exact midpoint comparisons repair the candidate; the old full division remains the cold
@@ -1298,7 +1350,7 @@ private static int compareDecimalToFloatBoundary(
private static int compareDecimalToBinary(
long unscaled, long divisor, long numerator, int binaryExponent) {
long productLow = divisor * numerator;
- long productHigh = multiplyHigh(divisor, numerator);
+ long productHigh = DecimalMath.unsignedMultiplyHigh(divisor, numerator);
int productBits = bitLength(productHigh, productLow);
int unscaledBits = Long.SIZE - Long.numberOfLeadingZeros(unscaled);
if (binaryExponent < 0) {
@@ -1333,17 +1385,6 @@ private static int doubleBinaryExponent(long bits) {
return exponent == 0 ? -1074 : exponent - 1075;
}
- private static long multiplyHigh(long x, long y) {
- long xlo = x & 0xffff_ffffL;
- long xhi = x >>> 32;
- long ylo = y & 0xffff_ffffL;
- long yhi = y >>> 32;
- long lowProduct = xlo * ylo;
- long carryProduct = xhi * ylo + (lowProduct >>> 32);
- long middle = (carryProduct & 0xffff_ffffL) + xlo * yhi;
- return xhi * yhi + (carryProduct >>> 32) + (middle >>> 32);
- }
-
protected final double readDoubleFallbackValue(int start) {
position = start;
if (start < length() && charAt(start) == '"') {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
index cbe2a16ec0..cf0e25c674 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
@@ -2416,6 +2416,29 @@ private long readQuotedStringHash() {
}
private long readQuotedStringHashToken() {
+ byte[] bytes = input;
+ int mark = position;
+ int nameOffset = mark + 1;
+ if (nameOffset + Long.BYTES < bytes.length && bytes[mark] == '"') {
+ long word = LittleEndian.getInt64(bytes, nameOffset);
+ long stopMask = asciiStringStopMask(word);
+ if (stopMask == 0) {
+ if (bytes[nameOffset + Long.BYTES] == '"') {
+ position = nameOffset + Long.BYTES + 1;
+ return word;
+ }
+ } else {
+ int nameLength = Long.numberOfTrailingZeros(stopMask) >>> 3;
+ if (nameLength > 0 && ((word >>> (nameLength << 3)) & 0xFF) == '"') {
+ position = nameOffset + nameLength + 1;
+ return word & ((1L << (nameLength << 3)) - 1);
+ }
+ }
+ }
+ return readQuotedStringHashSlow();
+ }
+
+ private long readQuotedStringHashSlow() {
byte[] bytes = input;
int length = bytes.length;
if (position >= length || bytes[position++] != '"') {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
index b6b9492cdf..ee796a9280 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
@@ -25,6 +25,7 @@
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.UUID;
+import org.apache.fory.annotation.Internal;
import org.apache.fory.json.JsonConfig;
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldNameHash;
@@ -56,27 +57,42 @@ public final class Utf8JsonReader extends JsonReader {
private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN;
private static final long BYTE_ONES = 0x0101010101010101L;
private static final int INT_BYTE_ONES = 0x01010101;
+ private static final long BYTE_TWOS = 0x0202020202020202L;
+ private static final int INT_BYTE_TWOS = 0x02020202;
private static final long BYTE_HIGH_BITS = 0x8080808080808080L;
private static final int INT_BYTE_HIGH_BITS = 0x80808080;
private static final long BACKSLASH_BYTES = 0x5c5c5c5c5c5c5c5cL;
private static final int INT_BACKSLASH_BYTES = 0x5c5c5c5c;
- private static final long CONTROL_LIMIT_BYTES = 0x2020202020202020L;
- private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020;
- private static final long QUOTE_BYTES = 0x2222222222222222L;
- private static final int INT_QUOTE_BYTES = 0x22222222;
+ private static final long QUOTE_CONTROL_LIMIT_BYTES = 0x2121212121212121L;
+ private static final int INT_QUOTE_CONTROL_LIMIT_BYTES = 0x21212121;
+ private static final int INT_MAX_DIV_10 = Integer.MAX_VALUE / 10;
+ private static final int INT_MAX_MOD_10 = Integer.MAX_VALUE % 10;
private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10;
private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10);
private static final long LONG_MAX_DIV_100 = Long.MAX_VALUE / 100;
private static final int LONG_MAX_MOD_100 = (int) (Long.MAX_VALUE % 100);
+ private static final long FOUR_DIGITS = 10_000L;
+ private static final long LONG_MAX_DIV_FOUR_DIGITS = Long.MAX_VALUE / FOUR_DIGITS;
private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10;
private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10);
private static final long EIGHT_DIGITS = 100_000_000L;
+ private static final long LONG_MAX_DIV_EIGHT_DIGITS = Long.MAX_VALUE / EIGHT_DIGITS;
+ private static final int LONG_MAX_MOD_EIGHT_DIGITS = (int) (Long.MAX_VALUE % EIGHT_DIGITS);
private static final long ASCII_ZEROES = 0x3030_3030_3030_3030L;
private static final long ASCII_NINES = 0x3939_3939_3939_3939L;
private static final long ASCII_HIGH_BITS = 0x8080_8080_8080_8080L;
// Little-endian packed ASCII bytes for "null".
private static final int NULL_LITERAL = 0x6C6C756E;
+ /** The generated String-array loop consumed the closing bracket. */
+ @Internal public static final int STRING_ARRAY_END = 0;
+
+ /** The generated String-array loop consumed both a comma and the next opening quote. */
+ @Internal public static final int STRING_ARRAY_QUOTED = 1;
+
+ /** The generated String-array loop consumed a comma and left the next value unread. */
+ @Internal public static final int STRING_ARRAY_VALUE = 2;
+
// JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly.
// UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content.
private byte[] input;
@@ -411,6 +427,18 @@ public boolean consumeNextToken(char expected) {
return consumeToken(expected);
}
+ /** Consumes a string quote without classifying whitespace, null, or malformed input. */
+ @Internal
+ public boolean tryConsumeStringQuote() {
+ byte[] bytes = input;
+ int offset = position;
+ if (offset < bytes.length && bytes[offset] == '"') {
+ position = offset + 1;
+ return true;
+ }
+ return false;
+ }
+
public void expectToken(char expected) {
if (!consumeToken(expected)) {
throw error("Expected '" + expected + "'");
@@ -477,6 +505,60 @@ public boolean consumeNextCommaOrEndArray() {
return consumeNextCommaOrEndArraySlow();
}
+ /**
+ * Consumes an array separator and, when adjacent, the next String's opening quote.
+ *
+ *
The concrete reader owns syntax and cursor publication. Generated exact String collections
+ * own value decoding and can therefore continue directly from the returned state without a second
+ * token probe.
+ */
+ @Internal
+ public int consumeNextStringArrayElement() {
+ byte[] bytes = input;
+ int offset = position;
+ if (offset < bytes.length) {
+ int ch = bytes[offset];
+ if (ch == ']') {
+ position = offset + 1;
+ return STRING_ARRAY_END;
+ }
+ if (ch == ',') {
+ offset++;
+ if (offset < bytes.length && bytes[offset] == '"') {
+ position = offset + 1;
+ return STRING_ARRAY_QUOTED;
+ }
+ position = offset;
+ return STRING_ARRAY_VALUE;
+ }
+ }
+ return consumeNextStringArrayElementSlow();
+ }
+
+ private int consumeNextStringArrayElementSlow() {
+ skipWhitespaceFast();
+ byte[] bytes = input;
+ int offset = position;
+ if (offset < bytes.length) {
+ int ch = bytes[offset];
+ if (ch == ']') {
+ position = offset + 1;
+ return STRING_ARRAY_END;
+ }
+ if (ch == ',') {
+ position = offset + 1;
+ skipWhitespaceFast();
+ offset = position;
+ if (offset < bytes.length && bytes[offset] == '"') {
+ position = offset + 1;
+ return STRING_ARRAY_QUOTED;
+ }
+ return STRING_ARRAY_VALUE;
+ }
+ }
+ throw error("Expected ',' or ']'");
+ }
+
private boolean consumeNextArrayEndOrSlow(int ch) {
if (ch == ']') {
position++;
@@ -636,18 +718,21 @@ private int readIntToken() {
}
private int readPositiveIntTail(byte[] bytes, int offset, int inputLength, int result) {
- while (offset < inputLength) {
+ // The caller has consumed exactly nine positive digits. A Java int can contain only one more;
+ // any following digit is necessarily overflow rather than another loop iteration.
+ int digit = bytes[offset] - '0';
+ if (result > INT_MAX_DIV_10 || (result == INT_MAX_DIV_10 && digit > INT_MAX_MOD_10)) {
+ position = offset;
+ throw error("Integer overflow");
+ }
+ result = result * 10 + digit;
+ offset++;
+ if (offset < inputLength) {
int ch = bytes[offset];
- if (ch < '0' || ch > '9') {
- break;
- }
- long value = (long) result * 10 + (ch - '0');
- if (value > Integer.MAX_VALUE) {
+ if (ch >= '0' && ch <= '9') {
position = offset;
throw error("Integer overflow");
}
- result = (int) value;
- offset++;
}
position = offset;
rejectFractionOrExponentFast();
@@ -767,6 +852,13 @@ public float readFloatTokenValue() {
return readFloatToken();
}
+ // Long parsing deliberately repeats the initial digit checks, zero handling, block scan, and
+ // short tail used by Int parsing instead of sharing one generic token loop. The widths have
+ // different safe digit counts, overflow rules, and runtime profiles; a small shared helper lets
+ // one profile determine both callers' inline layout and loses the width-specific locals. Keep
+ // malformed input and overflow in their cold tails. Do not deduplicate this common path without
+ // matched intrinsic and aggregate C2 evidence, and never add padding or benchmark-specific
+ // digit-count branches to create an inline boundary.
private long readLongToken() {
byte[] bytes = input;
int offset = position;
@@ -928,6 +1020,65 @@ private static int parseEightDigits(byte[] bytes, int offset, int safeEnd) {
return (int) ((quads & 0xFFFF) * 10_000 + (quads >>> 32));
}
+ private static int parseFourDigits(byte[] bytes, int offset, int safeEnd) {
+ if (offset + 4 > safeEnd) {
+ return -1;
+ }
+ int chunk = LittleEndian.getInt32(bytes, offset);
+ int digits = chunk - (int) ASCII_ZEROES;
+ if (((digits | ((int) ASCII_NINES - chunk)) & INT_BYTE_HIGH_BITS) != 0) {
+ return -1;
+ }
+ int pairs = (digits * 10 + (digits >>> 8)) & 0x00FF_00FF;
+ return (pairs & 0xFFFF) * 100 + (pairs >>> 16);
+ }
+
+ private static long appendEightDigits(byte[] bytes, int offset, int safeEnd, long unscaled) {
+ int block = parseEightDigits(bytes, offset, safeEnd);
+ if (block < 0 || !canAppendEightDigits(unscaled, block)) {
+ return -1;
+ }
+ return unscaled * EIGHT_DIGITS + block;
+ }
+
+ private static long appendFourDigits(byte[] bytes, int offset, int safeEnd, long unscaled) {
+ int block = parseFourDigits(bytes, offset, safeEnd);
+ if (block < 0) {
+ return -1;
+ }
+ // Callers use a strict divisor bound, so every validated four-digit block is safe here. The
+ // one equality boundary stays on the pair path because its final block determines overflow.
+ return unscaled * FOUR_DIGITS + block;
+ }
+
+ // Positive magnitudes below these power-of-two bounds can append the full decimal chunk without
+ // overflowing a signed long. On the high branch, adding the remainder carry converts the exact
+ // boundary into one unsigned divisor comparison; unsigned order also rejects MAX_VALUE + 1 after
+ // it wraps. The validated digit and pair ranges make the shifts exact zero-or-one carries.
+ private static boolean canAppendDigit(long unscaled, int digit) {
+ if ((unscaled >>> 59) == 0) {
+ return true;
+ }
+ long adjusted = unscaled + (digit >>> 3);
+ return Long.compareUnsigned(adjusted, LONG_MAX_DIV_10) <= 0;
+ }
+
+ private static boolean canAppendTwoDigits(long unscaled, int pair) {
+ if ((unscaled >>> 56) == 0) {
+ return true;
+ }
+ long adjusted = unscaled + ((pair + 120) >>> 7);
+ return Long.compareUnsigned(adjusted, LONG_MAX_DIV_100) <= 0;
+ }
+
+ private static boolean canAppendEightDigits(long unscaled, int block) {
+ if ((unscaled >>> 36) == 0) {
+ return true;
+ }
+ long adjusted = unscaled + (block > LONG_MAX_MOD_EIGHT_DIGITS ? 1 : 0);
+ return Long.compareUnsigned(adjusted, LONG_MAX_DIV_EIGHT_DIGITS) <= 0;
+ }
+
private BigDecimal readBigDecimalToken() {
byte[] bytes = input;
int offset = position;
@@ -1353,6 +1504,8 @@ private float readFloatFallback(int start) {
return readFloatFallbackValue(start);
}
+ // Keep the complete integer and fraction scan in one token owner. A separate inline-sized
+ // fraction tail makes generated callers depend on which method C2 compiles first.
private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength, int ch) {
int start = offset;
long unscaled = 0;
@@ -1374,8 +1527,7 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength
break;
}
int pair = high * 10 + low;
- if (unscaled > LONG_MAX_DIV_100
- || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) {
+ if (!canAppendTwoDigits(unscaled, pair)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 100 + pair;
@@ -1384,8 +1536,7 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength
if (offset < inputLength) {
int digit = bytes[offset] - '0';
if (digit >= 0 && digit <= 9) {
- if (unscaled > LONG_MAX_DIV_10
- || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) {
+ if (!canAppendDigit(unscaled, digit)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 10 + digit;
@@ -1395,29 +1546,25 @@ private double readPositiveDoubleToken(byte[] bytes, int offset, int inputLength
} else {
return readDoubleFallback(start);
}
- return readPositiveDoubleTail(bytes, offset, inputLength, start, unscaled);
- }
-
- private double readSignedDoubleToken(int start) {
- byte[] bytes = input;
- int offset = start + 1;
- int inputLength = bytes.length;
- if (offset >= inputLength) {
- return readDoubleFallback(start);
- }
- int ch = bytes[offset];
- long unscaled = 0;
- if (ch == '0') {
+ int scale = 0;
+ if (offset < inputLength && bytes[offset] == '.') {
offset++;
- if (offset < inputLength) {
- ch = bytes[offset];
- if (ch >= '0' && ch <= '9') {
- return readDoubleFallback(start);
+ int fractionStart = offset;
+ long appended = appendEightDigits(bytes, offset, inputLength, unscaled);
+ while (appended >= 0) {
+ unscaled = appended;
+ scale += 8;
+ offset += 8;
+ appended = appendEightDigits(bytes, offset, inputLength, unscaled);
+ }
+ if (scale != 0 && unscaled < LONG_MAX_DIV_FOUR_DIGITS) {
+ appended = appendFourDigits(bytes, offset, inputLength, unscaled);
+ if (appended >= 0) {
+ unscaled = appended;
+ scale += 4;
+ offset += 4;
}
}
- } else if (ch >= '1' && ch <= '9') {
- unscaled = ch - '0';
- offset++;
while (offset + 1 < inputLength) {
int high = bytes[offset] - '0';
int low = bytes[offset + 1] - '0';
@@ -1425,36 +1572,51 @@ private double readSignedDoubleToken(int start) {
break;
}
int pair = high * 10 + low;
- if (unscaled > LONG_MAX_DIV_100
- || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) {
+ if (!canAppendTwoDigits(unscaled, pair)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 100 + pair;
+ scale += 2;
offset += 2;
}
if (offset < inputLength) {
int digit = bytes[offset] - '0';
if (digit >= 0 && digit <= 9) {
- if (unscaled > LONG_MAX_DIV_10
- || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) {
+ if (!canAppendDigit(unscaled, digit)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 10 + digit;
+ scale++;
offset++;
}
}
- } else {
- return readDoubleFallback(start);
+ if (offset == fractionStart) {
+ return readDoubleFallback(start);
+ }
}
- return readSignedDoubleTail(bytes, offset, inputLength, start, unscaled);
+ return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale);
}
- private double readPositiveDoubleTail(
- byte[] bytes, int offset, int inputLength, int start, long unscaled) {
- int scale = 0;
- if (offset < inputLength && bytes[offset] == '.') {
+ private double readSignedDoubleToken(int start) {
+ byte[] bytes = input;
+ int offset = start + 1;
+ int inputLength = bytes.length;
+ if (offset >= inputLength) {
+ return readDoubleFallback(start);
+ }
+ int ch = bytes[offset];
+ long unscaled = 0;
+ if (ch == '0') {
+ offset++;
+ if (offset < inputLength) {
+ ch = bytes[offset];
+ if (ch >= '0' && ch <= '9') {
+ return readDoubleFallback(start);
+ }
+ }
+ } else if (ch >= '1' && ch <= '9') {
+ unscaled = ch - '0';
offset++;
- int fractionStart = offset;
while (offset + 1 < inputLength) {
int high = bytes[offset] - '0';
int low = bytes[offset + 1] - '0';
@@ -1462,39 +1624,44 @@ private double readPositiveDoubleTail(
break;
}
int pair = high * 10 + low;
- if (unscaled > LONG_MAX_DIV_100
- || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) {
+ if (!canAppendTwoDigits(unscaled, pair)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 100 + pair;
- scale += 2;
offset += 2;
}
if (offset < inputLength) {
int digit = bytes[offset] - '0';
if (digit >= 0 && digit <= 9) {
- if (unscaled > LONG_MAX_DIV_10
- || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) {
+ if (!canAppendDigit(unscaled, digit)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 10 + digit;
- scale++;
offset++;
}
}
- if (offset == fractionStart) {
- return readDoubleFallback(start);
- }
+ } else {
+ return readDoubleFallback(start);
}
- return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale);
- }
-
- private double readSignedDoubleTail(
- byte[] bytes, int offset, int inputLength, int start, long unscaled) {
int scale = 0;
if (offset < inputLength && bytes[offset] == '.') {
offset++;
int fractionStart = offset;
+ long appended = appendEightDigits(bytes, offset, inputLength, unscaled);
+ while (appended >= 0) {
+ unscaled = appended;
+ scale += 8;
+ offset += 8;
+ appended = appendEightDigits(bytes, offset, inputLength, unscaled);
+ }
+ if (scale != 0 && unscaled < LONG_MAX_DIV_FOUR_DIGITS) {
+ appended = appendFourDigits(bytes, offset, inputLength, unscaled);
+ if (appended >= 0) {
+ unscaled = appended;
+ scale += 4;
+ offset += 4;
+ }
+ }
while (offset + 1 < inputLength) {
int high = bytes[offset] - '0';
int low = bytes[offset + 1] - '0';
@@ -1502,8 +1669,7 @@ private double readSignedDoubleTail(
break;
}
int pair = high * 10 + low;
- if (unscaled > LONG_MAX_DIV_100
- || (unscaled == LONG_MAX_DIV_100 && pair > LONG_MAX_MOD_100)) {
+ if (!canAppendTwoDigits(unscaled, pair)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 100 + pair;
@@ -1513,8 +1679,7 @@ private double readSignedDoubleTail(
if (offset < inputLength) {
int digit = bytes[offset] - '0';
if (digit >= 0 && digit <= 9) {
- if (unscaled > LONG_MAX_DIV_10
- || (unscaled == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) {
+ if (!canAppendDigit(unscaled, digit)) {
return readDoubleFallback(start);
}
unscaled = unscaled * 10 + digit;
@@ -1934,6 +2099,30 @@ private String readStringWordStop(int start, int offset, long stopMask) {
return readStringStop(start, stop, b);
}
+ /** Returns the current UTF-8 input length to generated bounded String probes. */
+ @Internal
+ public int inputLength() {
+ return input.length;
+ }
+
+ /** Scans one in-bounds generated String word without publishing the reader cursor. */
+ @Internal
+ public long scanStringWord(int offset) {
+ return stringStopMask(LittleEndian.getInt64(input, offset));
+ }
+
+ /** Finishes a generated String after a bounded word probe finds its first stop byte. */
+ @Internal
+ public String finishStringWord(int start, int offset, long stopMask) {
+ return readStringWordStop(start, offset, stopMask);
+ }
+
+ /** Continues a String after generated bounded word probes found no stop byte. */
+ @Internal
+ public String readStringTokenLongTail(int start, int offset) {
+ return readStringTokenLongTail(start, offset, input.length);
+ }
+
private String readStringTokenLongTail(int start, int offset, int inputLength) {
byte[] bytes = input;
int doubleWordEnd = inputLength - (Long.BYTES << 1);
@@ -2235,14 +2424,18 @@ private static long stringStopMask(long word) {
// Subtraction borrow may only create later high bits after an earlier real stop, so the
// compact syntax/range expression preserves the first-stop position. Latin1JsonReader cannot
// use this shortcut because high-bit Latin-1 bytes are valid string payload.
- long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES);
- return (syntaxStop | word | (word - CONTROL_LIMIT_BYTES)) & BYTE_HIGH_BITS;
+ // XOR by 2 preserves control bytes below 0x20 and maps quote 0x22 to 0x20. One relaxed
+ // byte-lane comparison against 0x21 can therefore cover both cases without a separate quote
+ // zero detector; printable bytes before the first stop remain at or above the limit.
+ long quoteOrControl = (word ^ BYTE_TWOS) - QUOTE_CONTROL_LIMIT_BYTES;
+ long backslash = (word ^ BACKSLASH_BYTES) - BYTE_ONES;
+ return (quoteOrControl | backslash | word) & BYTE_HIGH_BITS;
}
private static int stringStopMask(int word) {
- int syntaxStop =
- ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES);
- return (syntaxStop | word | (word - INT_CONTROL_LIMIT_BYTES)) & INT_BYTE_HIGH_BITS;
+ int quoteOrControl = (word ^ INT_BYTE_TWOS) - INT_QUOTE_CONTROL_LIMIT_BYTES;
+ int backslash = (word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES;
+ return (quoteOrControl | backslash | word) & INT_BYTE_HIGH_BITS;
}
@Override
@@ -2522,6 +2715,29 @@ private long readQuotedStringHash() {
}
private long readQuotedStringHashToken() {
+ byte[] bytes = input;
+ int mark = position;
+ int nameOffset = mark + 1;
+ if (nameOffset + Long.BYTES < bytes.length && bytes[mark] == '"') {
+ long word = LittleEndian.getInt64(bytes, nameOffset);
+ long stopMask = stringStopMask(word);
+ if (stopMask == 0) {
+ if (bytes[nameOffset + Long.BYTES] == '"') {
+ position = nameOffset + Long.BYTES + 1;
+ return word;
+ }
+ } else {
+ int nameLength = Long.numberOfTrailingZeros(stopMask) >>> 3;
+ if (nameLength > 0 && ((word >>> (nameLength << 3)) & 0xFF) == '"') {
+ position = nameOffset + nameLength + 1;
+ return word & ((1L << (nameLength << 3)) - 1);
+ }
+ }
+ }
+ return readQuotedStringHashSlow();
+ }
+
+ private long readQuotedStringHashSlow() {
byte[] bytes = input;
int length = bytes.length;
if (position >= length || bytes[position++] != '"') {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java
index 237e669d86..4e382b3bda 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java
@@ -243,7 +243,8 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader(
ObjectCodec> owner,
JsonFieldTable readTable,
JsonFieldInfo[] fields,
- Latin1ReaderCodec[] codecs) {
+ Latin1ReaderCodec[] codecs,
+ Latin1ReaderCodec selfReader) {
try {
if (AndroidSupport.IS_ANDROID) {
Constructor> constructor =
@@ -251,10 +252,11 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader(
ObjectCodec.class,
JsonFieldTable.class,
JsonFieldInfo[].class,
- Latin1ReaderCodec[].class);
+ Latin1ReaderCodec[].class,
+ Latin1ReaderCodec.class);
constructor.setAccessible(true);
return (Latin1ReaderCodec)
- constructor.newInstance(owner, readTable, fields, codecs);
+ constructor.newInstance(owner, readTable, fields, codecs, selfReader);
}
MethodHandle constructor =
_JDKAccess._trustedLookup(type)
@@ -265,8 +267,10 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader(
ObjectCodec.class,
JsonFieldTable.class,
JsonFieldInfo[].class,
- Latin1ReaderCodec[].class));
- return (Latin1ReaderCodec) constructor.invoke(owner, readTable, fields, codecs);
+ Latin1ReaderCodec[].class,
+ Latin1ReaderCodec.class));
+ return (Latin1ReaderCodec)
+ constructor.invoke(owner, readTable, fields, codecs, selfReader);
} catch (Throwable e) {
throw new ForyJsonException("Cannot instantiate generated JSON Any Latin1 reader", e);
}
@@ -279,6 +283,7 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader(
JsonFieldTable readTable,
JsonFieldInfo[] fields,
Latin1ReaderCodec[] codecs,
+ Latin1ReaderCodec selfReader,
Latin1ReaderCodec anyCodec) {
try {
if (AndroidSupport.IS_ANDROID) {
@@ -288,10 +293,11 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader(
JsonFieldTable.class,
JsonFieldInfo[].class,
Latin1ReaderCodec[].class,
+ Latin1ReaderCodec.class,
Latin1ReaderCodec.class);
constructor.setAccessible(true);
return (Latin1ReaderCodec)
- constructor.newInstance(owner, readTable, fields, codecs, anyCodec);
+ constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec);
}
MethodHandle constructor =
_JDKAccess._trustedLookup(type)
@@ -303,9 +309,10 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader(
JsonFieldTable.class,
JsonFieldInfo[].class,
Latin1ReaderCodec[].class,
+ Latin1ReaderCodec.class,
Latin1ReaderCodec.class));
return (Latin1ReaderCodec)
- constructor.invoke(owner, readTable, fields, codecs, anyCodec);
+ constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec);
} catch (Throwable e) {
throw new ForyJsonException("Cannot instantiate generated JSON Any Latin1 reader", e);
}
@@ -346,7 +353,8 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader(
ObjectCodec> owner,
JsonFieldTable readTable,
JsonFieldInfo[] fields,
- Utf16ReaderCodec[] codecs) {
+ Utf16ReaderCodec[] codecs,
+ Utf16ReaderCodec selfReader) {
try {
if (AndroidSupport.IS_ANDROID) {
Constructor> constructor =
@@ -354,9 +362,11 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader(
ObjectCodec.class,
JsonFieldTable.class,
JsonFieldInfo[].class,
- Utf16ReaderCodec[].class);
+ Utf16ReaderCodec[].class,
+ Utf16ReaderCodec.class);
constructor.setAccessible(true);
- return (Utf16ReaderCodec) constructor.newInstance(owner, readTable, fields, codecs);
+ return (Utf16ReaderCodec)
+ constructor.newInstance(owner, readTable, fields, codecs, selfReader);
}
MethodHandle constructor =
_JDKAccess._trustedLookup(type)
@@ -367,8 +377,10 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader(
ObjectCodec.class,
JsonFieldTable.class,
JsonFieldInfo[].class,
- Utf16ReaderCodec[].class));
- return (Utf16ReaderCodec) constructor.invoke(owner, readTable, fields, codecs);
+ Utf16ReaderCodec[].class,
+ Utf16ReaderCodec.class));
+ return (Utf16ReaderCodec)
+ constructor.invoke(owner, readTable, fields, codecs, selfReader);
} catch (Throwable e) {
throw new ForyJsonException("Cannot instantiate generated JSON Any UTF16 reader", e);
}
@@ -381,6 +393,7 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader(
JsonFieldTable readTable,
JsonFieldInfo[] fields,
Utf16ReaderCodec[] codecs,
+ Utf16ReaderCodec selfReader,
Utf16ReaderCodec anyCodec) {
try {
if (AndroidSupport.IS_ANDROID) {
@@ -390,10 +403,11 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader(
JsonFieldTable.class,
JsonFieldInfo[].class,
Utf16ReaderCodec[].class,
+ Utf16ReaderCodec.class,
Utf16ReaderCodec.class);
constructor.setAccessible(true);
return (Utf16ReaderCodec)
- constructor.newInstance(owner, readTable, fields, codecs, anyCodec);
+ constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec);
}
MethodHandle constructor =
_JDKAccess._trustedLookup(type)
@@ -405,9 +419,10 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader(
JsonFieldTable.class,
JsonFieldInfo[].class,
Utf16ReaderCodec[].class,
+ Utf16ReaderCodec.class,
Utf16ReaderCodec.class));
return (Utf16ReaderCodec)
- constructor.invoke(owner, readTable, fields, codecs, anyCodec);
+ constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec);
} catch (Throwable e) {
throw new ForyJsonException("Cannot instantiate generated JSON Any UTF16 reader", e);
}
@@ -442,13 +457,71 @@ static Utf8ReaderCodec instantiateUtf8Reader(
}
}
+ @SuppressWarnings("unchecked")
+ static Utf8WriterCodec instantiateUtf8CollectionWriter(
+ Class> type, Utf8WriterCodec fallback) {
+ try {
+ if (AndroidSupport.IS_ANDROID) {
+ Constructor> constructor = type.getDeclaredConstructor(Utf8WriterCodec.class);
+ constructor.setAccessible(true);
+ return (Utf8WriterCodec) constructor.newInstance(fallback);
+ }
+ MethodHandle constructor =
+ _JDKAccess._trustedLookup(type)
+ .findConstructor(type, MethodType.methodType(void.class, Utf8WriterCodec.class));
+ return (Utf8WriterCodec) constructor.invoke(fallback);
+ } catch (Throwable e) {
+ throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ static Utf8WriterCodec instantiateUtf8CollectionWriter(
+ Class> type, Utf8WriterCodec fallback, Utf8WriterCodec elementWriter) {
+ try {
+ if (AndroidSupport.IS_ANDROID) {
+ Constructor> constructor =
+ type.getDeclaredConstructor(Utf8WriterCodec.class, Utf8WriterCodec.class);
+ constructor.setAccessible(true);
+ return (Utf8WriterCodec) constructor.newInstance(fallback, elementWriter);
+ }
+ MethodHandle constructor =
+ _JDKAccess._trustedLookup(type)
+ .findConstructor(
+ type,
+ MethodType.methodType(void.class, Utf8WriterCodec.class, Utf8WriterCodec.class));
+ return (Utf8WriterCodec) constructor.invoke(fallback, elementWriter);
+ } catch (Throwable e) {
+ throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ static Utf8ReaderCodec instantiateUtf8CollectionReader(
+ Class> type, Utf8ReaderCodec elementReader) {
+ try {
+ if (AndroidSupport.IS_ANDROID) {
+ Constructor> constructor = type.getDeclaredConstructor(Utf8ReaderCodec.class);
+ constructor.setAccessible(true);
+ return (Utf8ReaderCodec) constructor.newInstance(elementReader);
+ }
+ MethodHandle constructor =
+ _JDKAccess._trustedLookup(type)
+ .findConstructor(type, MethodType.methodType(void.class, Utf8ReaderCodec.class));
+ return (Utf8ReaderCodec) constructor.invoke(elementReader);
+ } catch (Throwable e) {
+ throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection reader", e);
+ }
+ }
+
@SuppressWarnings("unchecked")
static Utf8ReaderCodec instantiateAnyUtf8Reader(
Class> type,
ObjectCodec> owner,
JsonFieldTable readTable,
JsonFieldInfo[] fields,
- Utf8ReaderCodec[] codecs) {
+ Utf8ReaderCodec[] codecs,
+ Utf8ReaderCodec selfReader) {
try {
if (AndroidSupport.IS_ANDROID) {
Constructor> constructor =
@@ -456,9 +529,11 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader(
ObjectCodec.class,
JsonFieldTable.class,
JsonFieldInfo[].class,
- Utf8ReaderCodec[].class);
+ Utf8ReaderCodec[].class,
+ Utf8ReaderCodec.class);
constructor.setAccessible(true);
- return (Utf8ReaderCodec) constructor.newInstance(owner, readTable, fields, codecs);
+ return (Utf8ReaderCodec)
+ constructor.newInstance(owner, readTable, fields, codecs, selfReader);
}
MethodHandle constructor =
_JDKAccess._trustedLookup(type)
@@ -469,8 +544,10 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader(
ObjectCodec.class,
JsonFieldTable.class,
JsonFieldInfo[].class,
- Utf8ReaderCodec[].class));
- return (Utf8ReaderCodec) constructor.invoke(owner, readTable, fields, codecs);
+ Utf8ReaderCodec[].class,
+ Utf8ReaderCodec.class));
+ return (Utf8ReaderCodec)
+ constructor.invoke(owner, readTable, fields, codecs, selfReader);
} catch (Throwable e) {
throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 reader", e);
}
@@ -483,6 +560,7 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader(
JsonFieldTable readTable,
JsonFieldInfo[] fields,
Utf8ReaderCodec[] codecs,
+ Utf8ReaderCodec selfReader,
Utf8ReaderCodec anyCodec) {
try {
if (AndroidSupport.IS_ANDROID) {
@@ -492,10 +570,11 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader(
JsonFieldTable.class,
JsonFieldInfo[].class,
Utf8ReaderCodec[].class,
+ Utf8ReaderCodec.class,
Utf8ReaderCodec.class);
constructor.setAccessible(true);
return (Utf8ReaderCodec)
- constructor.newInstance(owner, readTable, fields, codecs, anyCodec);
+ constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec);
}
MethodHandle constructor =
_JDKAccess._trustedLookup(type)
@@ -507,9 +586,10 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader(
JsonFieldTable.class,
JsonFieldInfo[].class,
Utf8ReaderCodec[].class,
+ Utf8ReaderCodec.class,
Utf8ReaderCodec.class));
return (Utf8ReaderCodec)
- constructor.invoke(owner, readTable, fields, codecs, anyCodec);
+ constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec);
} catch (Throwable e) {
throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 reader", e);
}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
index cf359827b3..285bf4247b 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
@@ -30,6 +30,7 @@
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
@@ -80,6 +81,8 @@
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -91,6 +94,7 @@
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.regex.Pattern;
import org.apache.fory.annotation.Internal;
+import org.apache.fory.codegen.CodeGenerator;
import org.apache.fory.codegen.GeneratedClassNames;
import org.apache.fory.exception.InsecureException;
import org.apache.fory.json.ForyJsonException;
@@ -112,6 +116,7 @@
import org.apache.fory.json.codec.JsonValueCodec;
import org.apache.fory.json.codec.MapCodec;
import org.apache.fory.json.codec.MapKeyCodec;
+import org.apache.fory.json.codec.ObjectCodec;
import org.apache.fory.json.codec.ScalarCodecs;
import org.apache.fory.json.codec.SqlJsonCodecs;
import org.apache.fory.json.codegen.JsonCodegen;
@@ -143,9 +148,9 @@
* state. Common short field names admitted by reader-local caches are published here for
* best-effort String reference reuse across readers. Reader-local admission is the only field-name
* capacity gate; the shared field-name map has no explicit limit. Source-generated model companions
- * and JIT-generated classes are shared here; concrete JIT codec instances, ordinary type bindings,
- * JIT locks, and callbacks remain resolver-local. A fresh generic {@link JsonJITContext} is
- * therefore created for every pooled JSON state.
+ * and JIT-generated class futures are shared here; concrete JIT codec instances, ordinary type
+ * bindings, graph construction, JIT locks, and publication remain resolver-local. A fresh {@link
+ * JsonJITContext} is therefore created for every pooled JSON state.
*/
public final class JsonSharedRegistry {
private static final int TYPE_CHECK_CACHE_LIMIT = 8192;
@@ -185,6 +190,13 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) {
private final ConcurrentHashMap, MapKeyCodec> mapKeyCodecs;
private final ConcurrentHashMap, GeneratedJsonCodec>> generatedCodecs;
private final Set> typesWithoutGeneratedCodec;
+ private final ConcurrentHashMap, CompletableFuture>> stringWriterClasses;
+ private final ConcurrentHashMap, CompletableFuture>> utf8WriterClasses;
+ private final ConcurrentHashMap, CompletableFuture>> latin1ReaderClasses;
+ private final ConcurrentHashMap, CompletableFuture>> utf16ReaderClasses;
+ private final ConcurrentHashMap, CompletableFuture>> utf8ReaderClasses;
+ private final ConcurrentHashMap>> utf8CollectionWriterClasses;
+ private final ConcurrentHashMap>> utf8CollectionReaderClasses;
private final ConcurrentHashMap cachedFieldNames;
public JsonSharedRegistry(JsonConfig config) {
@@ -214,6 +226,13 @@ public JsonSharedRegistry(JsonConfig config) {
mapKeyCodecs = new ConcurrentHashMap<>();
generatedCodecs = new ConcurrentHashMap<>();
typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet();
+ stringWriterClasses = new ConcurrentHashMap<>();
+ utf8WriterClasses = new ConcurrentHashMap<>();
+ latin1ReaderClasses = new ConcurrentHashMap<>();
+ utf16ReaderClasses = new ConcurrentHashMap<>();
+ utf8ReaderClasses = new ConcurrentHashMap<>();
+ utf8CollectionWriterClasses = new ConcurrentHashMap<>();
+ utf8CollectionReaderClasses = new ConcurrentHashMap<>();
cachedFieldNames = new ConcurrentHashMap<>();
boolean codegenEnabled = config.codegenEnabled();
codegen = codegenEnabled ? new JsonCodegen(config.getCodegenHash(), classLoader) : null;
@@ -222,6 +241,87 @@ public JsonSharedRegistry(JsonConfig config) {
registerExactCodecs();
}
+ CompletableFuture> stringWriterClass(ObjectCodec> owner, JsonTypeResolver resolver) {
+ return generatedClassFuture(
+ stringWriterClasses, owner.type(), () -> codegen.compileStringWriter(owner, resolver));
+ }
+
+ CompletableFuture> utf8WriterClass(ObjectCodec> owner, JsonTypeResolver resolver) {
+ return generatedClassFuture(
+ utf8WriterClasses, owner.type(), () -> codegen.compileUtf8Writer(owner, resolver));
+ }
+
+ CompletableFuture> latin1ReaderClass(ObjectCodec> owner, JsonTypeResolver resolver) {
+ return generatedClassFuture(
+ latin1ReaderClasses, owner.type(), () -> codegen.compileLatin1Reader(owner, resolver));
+ }
+
+ CompletableFuture> utf16ReaderClass(ObjectCodec> owner, JsonTypeResolver resolver) {
+ return generatedClassFuture(
+ utf16ReaderClasses, owner.type(), () -> codegen.compileUtf16Reader(owner, resolver));
+ }
+
+ CompletableFuture> utf8ReaderClass(ObjectCodec> owner, JsonTypeResolver resolver) {
+ return generatedClassFuture(
+ utf8ReaderClasses, owner.type(), () -> codegen.compileUtf8Reader(owner, resolver, true));
+ }
+
+ CompletableFuture> utf8CollectionWriterClass(
+ Type declaredType, CollectionCodec> owner) {
+ return generatedClassFuture(
+ utf8CollectionWriterClasses,
+ declaredType,
+ () -> codegen.compileUtf8CollectionWriter(declaredType, owner));
+ }
+
+ CompletableFuture> utf8CollectionReaderClass(
+ Type declaredType, CollectionCodec> owner) {
+ return generatedClassFuture(
+ utf8CollectionReaderClasses,
+ declaredType,
+ () -> codegen.compileUtf8CollectionReader(declaredType, owner));
+ }
+
+ private CompletableFuture> generatedClassFuture(
+ ConcurrentHashMap>> classes,
+ K key,
+ Callable> compiler) {
+ CompletableFuture> existing = classes.get(key);
+ if (existing != null) {
+ return existing;
+ }
+ CompletableFuture> candidate = new CompletableFuture<>();
+ existing = classes.putIfAbsent(key, candidate);
+ if (existing != null) {
+ return existing;
+ }
+ Runnable task =
+ () -> {
+ try {
+ candidate.complete(compiler.call());
+ } catch (Throwable failure) {
+ classes.remove(key, candidate);
+ candidate.completeExceptionally(failure);
+ }
+ };
+ if (!asyncCompilationEnabled) {
+ task.run();
+ return candidate;
+ }
+ ExecutorService service = compilationService;
+ if (service == null) {
+ service = CodeGenerator.getCompilationService();
+ }
+ try {
+ service.execute(task);
+ } catch (RuntimeException | Error failure) {
+ classes.remove(key, candidate);
+ candidate.completeExceptionally(failure);
+ throw failure;
+ }
+ return candidate;
+ }
+
/** Returns the immutable cached entry for {@code hash}, or null when none was published. */
@Internal
public CachedFieldName cachedFieldName(long hash) {
@@ -756,7 +856,7 @@ public JsonFieldKind kind(Class> type) {
}
JsonJITContext newJITContext() {
- return new JsonJITContext(asyncCompilationEnabled, compilationService);
+ return new JsonJITContext(asyncCompilationEnabled);
}
JsonCodegen codegen() {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java
index 95024dfcd9..bdd3804e5a 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java
@@ -39,10 +39,11 @@
* dispatch. These five fields are the sole installed capability state; there are no parallel
* per-path maps to reconcile on the hot path.
*
- * Only the canonical exact raw-class {@link ObjectCodec} binding may receive generated
- * replacements. Custom codecs, parameterized object bindings, containers, scalars, and dynamic
- * {@code Object} bindings retain their original semantic owner. Each complete capability is
- * installed in its own independently lazy slot.
+ *
{@link JsonTypeResolver} owns canonical exact raw-class {@link ObjectCodec} identity and the
+ * corresponding stable metadata owner. This binding stores only installed capabilities; custom
+ * codecs, parameterized object bindings, containers, scalars, and dynamic {@code Object} bindings
+ * retain their original semantic owner. Each complete capability is installed in its own
+ * independently lazy slot.
*/
public final class JsonTypeInfo {
private final Type type;
@@ -53,7 +54,6 @@ public final class JsonTypeInfo {
private Latin1ReaderCodec latin1Reader;
private Utf16ReaderCodec utf16Reader;
private Utf8ReaderCodec utf8Reader;
- private final boolean defaultObjectCodec;
private final boolean annotationCodec;
JsonTypeInfo(Type type, Class> rawType, JsonFieldKind kind, JsonValueCodec codec) {
@@ -75,9 +75,6 @@ public final class JsonTypeInfo {
latin1Reader = codec;
utf16Reader = codec;
utf8Reader = codec;
- // Only the raw-class ObjectCodec can be replaced by raw-class generated capabilities.
- // ParameterizedObjectCodec owns binding-specific field types and must remain the slot owner.
- defaultObjectCodec = codec.getClass() == ObjectCodec.class;
}
public Type type() {
@@ -132,10 +129,6 @@ void setUtf8Reader(Utf8ReaderCodec utf8Reader) {
this.utf8Reader = utf8Reader;
}
- public boolean usesDefaultObjectCodec() {
- return defaultObjectCodec;
- }
-
@Internal
public boolean usesAnnotationCodec() {
return annotationCodec;
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
index d054f6561c..8b59697c6c 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
@@ -19,13 +19,13 @@
package org.apache.fory.json.resolver;
-import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -33,6 +33,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import org.apache.fory.annotation.Internal;
@@ -65,19 +66,18 @@
import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldKind;
import org.apache.fory.json.meta.JsonFieldTable;
-import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
/**
* Local JSON type dispatcher used exclusively by one borrowed {@code ForyJson} state at a time.
*
- * This class corresponds to Fory core's {@code ClassResolver}: it owns terminal capabilities,
- * generated codec construction, capability-slot publication, and generated parent child-field
- * callbacks. Root codec execution and completion callbacks use the same resolver-local JIT lock.
- * {@link JsonJITContext} only orders generic JIT and notify callbacks under that lock; it does not
- * know any JSON capability, codec, generated class, or field metadata. Compilation failure leaves
- * the interpreted capability in its {@link JsonTypeInfo} slot; no parallel requested or failure
- * state is retained, so a later operation may retry compilation.
+ *
This class corresponds to Fory core's {@code ClassResolver}: it owns terminal capability
+ * construction, final child wiring, and capability-slot publication. After an outer metadata
+ * resolution succeeds, it registers every eligible representation graph once. Root codec execution
+ * and graph completion use the same resolver-local JIT lock. {@link JsonJITContext} only orders the
+ * completion under that lock; it does not know any JSON capability, generated class, or field
+ * metadata. Compilation failure leaves the interpreted capability in its {@link JsonTypeInfo} slot
+ * without adding failure or request state to the hot codec.
*
*
{@code typeInfos} owns declared and parameterized bindings. {@code objectCodecs} breaks
* recursive object-metadata construction by publishing the complete object owner before resolving
@@ -94,6 +94,7 @@ public final class JsonTypeResolver {
private final JsonJITContext jitContext;
private final IdentityMap, JsonTypeInfo> rawObjectTypeInfos;
private final IdentityMap, JsonTypeInfo> canonicalObjectTypeInfos;
+ private final IdentityMap> collectionCodecs;
private int resolutionDepth;
private enum RuntimeObjectKey {
@@ -108,6 +109,7 @@ public JsonTypeResolver(JsonSharedRegistry sharedRegistry) {
jitContext = sharedRegistry.newJITContext();
rawObjectTypeInfos = new IdentityMap<>();
canonicalObjectTypeInfos = new IdentityMap<>();
+ collectionCodecs = new IdentityMap<>();
}
/** Returns the shared registry that owns this resolver and its reader cache domain. */
@@ -145,7 +147,9 @@ private ObjectCodec getObjectCodec(TypeRef ownerType, Object key) {
}
ResolutionSnapshot snapshot = beginResolution();
try {
- return buildObjectCodec(ownerType, key);
+ ObjectCodec result = buildObjectCodec(ownerType, key);
+ completeResolution(snapshot);
+ return result;
} catch (RuntimeException | Error e) {
rollbackResolution(snapshot);
throw e;
@@ -162,7 +166,7 @@ public ObjectCodec> getUnwrappedObjectCodec(Class> rawType) {
if (typeInfo != null) {
// Generated capabilities replace type-info slots; objectCodecs retains the stable metadata
// owner used to build an unwrapped parent.
- return typeInfo.usesDefaultObjectCodec() ? objectCodecs.get(key) : null;
+ return canonicalObjectCodec(typeInfo);
}
if (customTypeInfo(rawType, rawType) != null || sharedRegistry.subTypesInfo(rawType) != null) {
return null;
@@ -178,10 +182,109 @@ public ObjectCodec> getUnwrappedObjectCodec(Class> rawType) {
}
typeInfo = newTypeInfo(rawType, rawType, codec);
typeInfos.put(key, typeInfo);
- registerObjectTypeInfo(typeInfo);
+ registerTypeInfoOwner(typeInfo, codec);
return codec;
}
+ /**
+ * Returns the stable metadata owner for a canonical raw-class object binding, or {@code null}.
+ *
+ * This lookup never constructs metadata or requests compilation. Source generation may call it
+ * outside a root operation; the short resolver-local lock protects the ordinary owner maps
+ * without coupling one generated class compilation to another.
+ */
+ @Internal
+ public ObjectCodec> canonicalObjectCodec(JsonTypeInfo typeInfo) {
+ jitContext.lock();
+ try {
+ return canonicalObjectOwner(typeInfo);
+ } finally {
+ jitContext.unlock();
+ }
+ }
+
+ private ObjectCodec> canonicalObjectOwner(JsonTypeInfo typeInfo) {
+ if (rawObjectTypeInfos.get(typeInfo.rawType()) != typeInfo) {
+ return null;
+ }
+ return objectCodecs.get(typeInfo.rawType());
+ }
+
+ /** Returns an exact declared ArrayList-backed UTF-8 collection owner, or {@code null}. */
+ @Internal
+ public CollectionCodec> exactUtf8Collection(JsonTypeInfo typeInfo) {
+ jitContext.lock();
+ try {
+ return exactUtf8CollectionOwner(typeInfo);
+ } finally {
+ jitContext.unlock();
+ }
+ }
+
+ private CollectionCodec> exactUtf8CollectionOwner(JsonTypeInfo typeInfo) {
+ CollectionCodec> owner = exactDeclaredCollectionOwner(typeInfo);
+ if (owner == null || !owner.createsArrayList()) {
+ return null;
+ }
+ JsonTypeInfo element = declaredCollectionElement(typeInfo);
+ if (canonicalObjectOwner(element) == null
+ && !(owner instanceof CollectionCodec.DirectCollectionCodec)) {
+ return null;
+ }
+ return owner;
+ }
+
+ /** Returns an exact declared UTF-8 collection writer owner, or {@code null}. */
+ @Internal
+ public CollectionCodec> exactUtf8WriterCollection(JsonTypeInfo typeInfo) {
+ jitContext.lock();
+ try {
+ return exactUtf8WriterCollectionOwner(typeInfo);
+ } finally {
+ jitContext.unlock();
+ }
+ }
+
+ private CollectionCodec> exactUtf8WriterCollectionOwner(JsonTypeInfo typeInfo) {
+ CollectionCodec> owner = exactDeclaredCollectionOwner(typeInfo);
+ // A generated collection writer owns only the ArrayList common loop. Keep declarations that
+ // cannot legally contain an ArrayList on their existing codec instead of compiling a class
+ // whose common branch is unreachable.
+ if (owner == null || !typeInfo.rawType().isAssignableFrom(ArrayList.class)) {
+ return null;
+ }
+ if (owner instanceof CollectionCodec.DirectCollectionCodec) {
+ return owner;
+ }
+ JsonTypeInfo element = declaredCollectionElement(typeInfo);
+ return owner instanceof CollectionCodec.ObjectCollectionCodec
+ && canonicalObjectOwner(element) != null
+ ? owner
+ : null;
+ }
+
+ private CollectionCodec> exactDeclaredCollectionOwner(JsonTypeInfo typeInfo) {
+ CollectionCodec> owner = collectionCodecs.get(typeInfo);
+ if (owner == null || !(typeInfo.type() instanceof ParameterizedType)) {
+ return null;
+ }
+ JsonTypeInfo element = declaredCollectionElement(typeInfo);
+ Type declaredElement = CodecUtils.elementType(typeInfo.type());
+ if (element == null
+ || element.rawType() == Object.class
+ || element.usesAnnotationCodec()
+ || !declaredElement.equals(element.type())) {
+ return null;
+ }
+ return owner;
+ }
+
+ private JsonTypeInfo declaredCollectionElement(JsonTypeInfo collection) {
+ Type elementType = CodecUtils.elementType(collection.type());
+ Class> rawType = CodecUtils.rawType(elementType, Object.class);
+ return typeInfos.get(typeInfoKey(elementType, rawType));
+ }
+
public JsonTypeInfo getTypeInfo(Type declaredType, Class> fallback) {
Class> rawType = CodecUtils.rawType(declaredType, fallback);
Object key = typeInfoKey(declaredType, rawType);
@@ -191,7 +294,9 @@ public JsonTypeInfo getTypeInfo(Type declaredType, Class> fallback) {
}
ResolutionSnapshot snapshot = beginResolution();
try {
- return resolveTypeInfo(declaredType, rawType, key);
+ JsonTypeInfo result = resolveTypeInfo(declaredType, rawType, key);
+ completeResolution(snapshot);
+ return result;
} catch (RuntimeException | Error e) {
rollbackResolution(snapshot);
throw e;
@@ -208,7 +313,9 @@ public JsonTypeInfo getTypeInfo(Type declaredType, Class> fallback, JsonCodec
Class> rawType = CodecUtils.rawType(declaredType, fallback);
ResolutionSnapshot snapshot = beginResolution();
try {
- return resolveTypeInfo(declaredType, rawType, annotation);
+ JsonTypeInfo result = resolveTypeInfo(declaredType, rawType, annotation);
+ completeResolution(snapshot);
+ return result;
} catch (RuntimeException | Error e) {
rollbackResolution(snapshot);
throw e;
@@ -301,7 +408,7 @@ private JsonTypeInfo resolveTypeInfo(Type declaredType, Class> rawType, JsonCo
return newTypeInfo(
declaredType,
rawType,
- CollectionCodec.create(rawType, elementType.getRawType(), elementInfo));
+ CollectionCodec.create(rawType, elementType.getRawType(), elementInfo, this));
}
if (Map.class.isAssignableFrom(rawType)) {
if (hasElement || hasContent || !hasKey && !hasMapValue) {
@@ -428,6 +535,21 @@ private void endResolution() {
resolutionDepth--;
}
+ private void completeResolution(ResolutionSnapshot snapshot) {
+ if (snapshot == null || codegen == null) {
+ return;
+ }
+ ArrayList roots = new ArrayList<>();
+ for (Map.Entry entry : typeInfos.entrySet()) {
+ if (!snapshot.typeKeys.contains(entry.getKey())) {
+ roots.add(entry.getValue());
+ }
+ }
+ if (!roots.isEmpty()) {
+ requestCapabilities(roots);
+ }
+ }
+
private void rollbackResolution(ResolutionSnapshot snapshot) {
if (snapshot == null) {
return;
@@ -447,6 +569,7 @@ private void rollbackResolution(ResolutionSnapshot snapshot) {
canonicalObjectTypeInfos.remove(owner);
}
}
+ collectionCodecs.remove(value);
typeIterator.remove();
}
}
@@ -476,7 +599,9 @@ public JsonTypeInfo getRuntimeTypeInfo(Class> runtimeType) {
}
ResolutionSnapshot snapshot = beginResolution();
try {
- return resolveRuntimeTypeInfo(runtimeType, key);
+ JsonTypeInfo result = resolveRuntimeTypeInfo(runtimeType, key);
+ completeResolution(snapshot);
+ return result;
} catch (RuntimeException | Error e) {
rollbackResolution(snapshot);
throw e;
@@ -486,14 +611,28 @@ public JsonTypeInfo getRuntimeTypeInfo(Class> runtimeType) {
}
private JsonTypeInfo resolveRuntimeTypeInfo(Class> runtimeType, Object key) {
- JsonTypeInfo typeInfo;
- typeInfo = buildRuntimeTypeInfo(runtimeType);
+ JsonTypeInfo typeInfo = customTypeInfo(runtimeType, runtimeType);
+ JsonValueCodec> codec = null;
+ if (typeInfo == null) {
+ sharedRegistry.checkSecure(runtimeType);
+ TypeRef> typeRef = TypeRef.of(runtimeType);
+ codec =
+ runtimeType == Object.class
+ ? getObjectCodec(typeRef)
+ : sharedRegistry.createCodec(runtimeType, typeRef, this);
+ if (codec == null) {
+ codec = getObjectCodec(typeRef);
+ }
+ typeInfo = newTypeInfo(runtimeType, runtimeType, codec);
+ }
JsonTypeInfo recursiveTypeInfo = typeInfos.get(key);
if (recursiveTypeInfo != null) {
return recursiveTypeInfo;
}
typeInfos.put(key, typeInfo);
- registerObjectTypeInfo(typeInfo);
+ if (codec != null) {
+ registerTypeInfoOwner(typeInfo, codec);
+ }
return typeInfo;
}
@@ -514,28 +653,7 @@ public StringWriterCodec stringWriter(ObjectCodec codec) {
if (typeInfo == null) {
return codec;
}
- StringWriterCodec installed = typeInfo.stringWriter();
- if (installed == owner && codegen != null && codegen.canCompileWriter(owner)) {
- jitContext.registerJITCallback(
- () -> owner.getClass(),
- () -> codegen.compileStringWriter(owner),
- new JsonJITContext.JITCallback>() {
- @Override
- public void onSuccess(Class> generated) {
- publishStringWriter(owner, typeInfo, generated);
- }
-
- @Override
- public void onFailure(Throwable failure) {}
-
- @Override
- public Object id() {
- return codegen.stringWriterJITId(owner.type());
- }
- });
- installed = typeInfo.stringWriter();
- }
- return (StringWriterCodec) installed;
+ return (StringWriterCodec) typeInfo.stringWriter();
}
@SuppressWarnings("unchecked")
@@ -546,28 +664,7 @@ public Utf8WriterCodec utf8Writer(ObjectCodec codec) {
if (typeInfo == null) {
return codec;
}
- Utf8WriterCodec installed = typeInfo.utf8Writer();
- if (installed == owner && codegen != null && codegen.canCompileWriter(owner)) {
- jitContext.registerJITCallback(
- () -> owner.getClass(),
- () -> codegen.compileUtf8Writer(owner),
- new JsonJITContext.JITCallback>() {
- @Override
- public void onSuccess(Class> generated) {
- publishUtf8Writer(owner, typeInfo, generated);
- }
-
- @Override
- public void onFailure(Throwable failure) {}
-
- @Override
- public Object id() {
- return codegen.utf8WriterJITId(owner.type());
- }
- });
- installed = typeInfo.utf8Writer();
- }
- return (Utf8WriterCodec) installed;
+ return (Utf8WriterCodec) typeInfo.utf8Writer();
}
@SuppressWarnings("unchecked")
@@ -578,28 +675,7 @@ public Latin1ReaderCodec latin1Reader(ObjectCodec codec) {
if (typeInfo == null) {
return codec;
}
- Latin1ReaderCodec installed = typeInfo.latin1Reader();
- if (installed == owner && codegen != null && codegen.canCompileReader(owner)) {
- jitContext.registerJITCallback(
- () -> owner.getClass(),
- () -> codegen.compileLatin1Reader(owner),
- new JsonJITContext.JITCallback>() {
- @Override
- public void onSuccess(Class> generated) {
- publishLatin1Reader(owner, typeInfo, generated);
- }
-
- @Override
- public void onFailure(Throwable failure) {}
-
- @Override
- public Object id() {
- return codegen.latin1ReaderJITId(owner.type());
- }
- });
- installed = typeInfo.latin1Reader();
- }
- return (Latin1ReaderCodec) installed;
+ return (Latin1ReaderCodec) typeInfo.latin1Reader();
}
@SuppressWarnings("unchecked")
@@ -610,28 +686,7 @@ public Utf16ReaderCodec utf16Reader(ObjectCodec codec) {
if (typeInfo == null) {
return codec;
}
- Utf16ReaderCodec installed = typeInfo.utf16Reader();
- if (installed == owner && codegen != null && codegen.canCompileReader(owner)) {
- jitContext.registerJITCallback(
- () -> owner.getClass(),
- () -> codegen.compileUtf16Reader(owner),
- new JsonJITContext.JITCallback>() {
- @Override
- public void onSuccess(Class> generated) {
- publishUtf16Reader(owner, typeInfo, generated);
- }
-
- @Override
- public void onFailure(Throwable failure) {}
-
- @Override
- public Object id() {
- return codegen.utf16ReaderJITId(owner.type());
- }
- });
- installed = typeInfo.utf16Reader();
- }
- return (Utf16ReaderCodec) installed;
+ return (Utf16ReaderCodec) typeInfo.utf16Reader();
}
@SuppressWarnings("unchecked")
@@ -642,150 +697,16 @@ public Utf8ReaderCodec utf8Reader(ObjectCodec codec) {
if (typeInfo == null) {
return codec;
}
- Utf8ReaderCodec installed = typeInfo.utf8Reader();
- if (installed == owner && codegen != null && codegen.canCompileReader(owner)) {
- jitContext.registerJITCallback(
- () -> owner.getClass(),
- () -> codegen.compileUtf8Reader(owner),
- new JsonJITContext.JITCallback>() {
- @Override
- public void onSuccess(Class> generated) {
- publishUtf8Reader(owner, typeInfo, generated);
- }
-
- @Override
- public void onFailure(Throwable failure) {}
-
- @Override
- public Object id() {
- return codegen.utf8ReaderJITId(owner.type());
- }
- });
- installed = typeInfo.utf8Reader();
- }
- return (Utf8ReaderCodec) installed;
- }
-
- @Internal
- public void resolveInlineAnyReaders(
- ClosedSubtypeCodec parent, int index, ObjectCodec> codec, JsonFieldTable readTable) {
- requireJITLock();
- ObjectCodec owner = erase(codec);
- JsonTypeInfo typeInfo = canonicalObjectTypeInfos.get(owner);
- if (typeInfo == null || codegen == null || !codegen.canCompileReader(owner)) {
- return;
- }
- resolveInlineLatin1Reader(parent, index, owner, typeInfo, readTable);
- resolveInlineUtf16Reader(parent, index, owner, typeInfo, readTable);
- resolveInlineUtf8Reader(parent, index, owner, typeInfo, readTable);
- }
-
- private void resolveInlineLatin1Reader(
- ClosedSubtypeCodec parent,
- int index,
- ObjectCodec owner,
- JsonTypeInfo typeInfo,
- JsonFieldTable readTable) {
- Latin1ReaderCodec current = latin1Reader(owner);
- if (current != owner) {
- parent.setInlineLatin1Reader(
- index, newInlineLatin1Reader(owner, current.getClass(), readTable, current));
- return;
- }
- jitContext.registerJITNotifyCallback(
- codegen.latin1ReaderJITId(owner.type()),
- new JsonJITContext.NotifyCallback() {
- @Override
- public void onNotifyResult(Object result) {
- Latin1ReaderCodec installed = typeInfo.latin1Reader();
- checkGeneratedClass(result, installed);
- parent.setInlineLatin1Reader(
- index, newInlineLatin1Reader(owner, (Class>) result, readTable, installed));
- }
-
- @Override
- public void onNotifyMissed() {
- Latin1ReaderCodec installed = typeInfo.latin1Reader();
- if (installed != owner) {
- parent.setInlineLatin1Reader(
- index, newInlineLatin1Reader(owner, installed.getClass(), readTable, installed));
- }
- }
- });
- }
-
- private void resolveInlineUtf16Reader(
- ClosedSubtypeCodec parent,
- int index,
- ObjectCodec owner,
- JsonTypeInfo typeInfo,
- JsonFieldTable readTable) {
- Utf16ReaderCodec current = utf16Reader(owner);
- if (current != owner) {
- parent.setInlineUtf16Reader(
- index, newInlineUtf16Reader(owner, current.getClass(), readTable, current));
- return;
- }
- jitContext.registerJITNotifyCallback(
- codegen.utf16ReaderJITId(owner.type()),
- new JsonJITContext.NotifyCallback() {
- @Override
- public void onNotifyResult(Object result) {
- Utf16ReaderCodec installed = typeInfo.utf16Reader();
- checkGeneratedClass(result, installed);
- parent.setInlineUtf16Reader(
- index, newInlineUtf16Reader(owner, (Class>) result, readTable, installed));
- }
-
- @Override
- public void onNotifyMissed() {
- Utf16ReaderCodec installed = typeInfo.utf16Reader();
- if (installed != owner) {
- parent.setInlineUtf16Reader(
- index, newInlineUtf16Reader(owner, installed.getClass(), readTable, installed));
- }
- }
- });
- }
-
- private void resolveInlineUtf8Reader(
- ClosedSubtypeCodec parent,
- int index,
- ObjectCodec owner,
- JsonTypeInfo typeInfo,
- JsonFieldTable readTable) {
- Utf8ReaderCodec current = utf8Reader(owner);
- if (current != owner) {
- parent.setInlineUtf8Reader(
- index, newInlineUtf8Reader(owner, current.getClass(), readTable, current));
- return;
- }
- jitContext.registerJITNotifyCallback(
- codegen.utf8ReaderJITId(owner.type()),
- new JsonJITContext.NotifyCallback() {
- @Override
- public void onNotifyResult(Object result) {
- Utf8ReaderCodec installed = typeInfo.utf8Reader();
- checkGeneratedClass(result, installed);
- parent.setInlineUtf8Reader(
- index, newInlineUtf8Reader(owner, (Class>) result, readTable, installed));
- }
-
- @Override
- public void onNotifyMissed() {
- Utf8ReaderCodec installed = typeInfo.utf8Reader();
- if (installed != owner) {
- parent.setInlineUtf8Reader(
- index, newInlineUtf8Reader(owner, installed.getClass(), readTable, installed));
- }
- }
- });
+ return (Utf8ReaderCodec) typeInfo.utf8Reader();
}
@SuppressWarnings("unchecked")
- private StringWriterCodec newStringWriter(ObjectCodec> owner, Class> generatedClass) {
+ private StringWriterCodec newStringWriter(
+ ObjectCodec> owner,
+ Class> generatedClass,
+ IdentityMap