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 capabilities) { if (owner.unwrappedInfo() != null) { - return newUnwrappedStringWriter(owner, generatedClass); + return newUnwrappedStringWriter(owner, generatedClass, capabilities); } JsonFieldInfo[] fields = owner.writeFields(); StringWriterCodec[] codecs = @@ -794,13 +715,7 @@ private StringWriterCodec newStringWriter(ObjectCodec owner, Class JsonFieldInfo field = fields[i]; if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); - StringWriterCodec codec = typeInfo.stringWriter(); - if (JsonCodegen.writeNestedType(field) != null - && typeInfo.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = stringWriter((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.STRING_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -811,35 +726,28 @@ private StringWriterCodec newStringWriter(ObjectCodec owner, Class return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs); } - StringWriterCodec anyCodec = any.valueTypeInfo().stringWriter(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = stringWriter((ObjectCodec) anyCodec); - } + StringWriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.STRING_WRITER); return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs, anyCodec); } @SuppressWarnings("unchecked") - private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class generatedClass) { + private Utf8WriterCodec newUtf8Writer( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { if (owner.unwrappedInfo() != null) { - return newUnwrappedUtf8Writer(owner, generatedClass); + return newUnwrappedUtf8Writer(owner, generatedClass, capabilities); } JsonFieldInfo[] fields = owner.writeFields(); Utf8WriterCodec[] codecs = (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field)) { + if (JsonCodegen.usesUtf8WriteCodec(field, this)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); - Utf8WriterCodec codec = typeInfo.utf8Writer(); - if (JsonCodegen.writeNestedType(field) != null - && typeInfo.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf8Writer((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -850,19 +758,17 @@ private Utf8WriterCodec newUtf8Writer(ObjectCodec owner, Class gen return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs); } - Utf8WriterCodec anyCodec = any.valueTypeInfo().utf8Writer(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Writer((ObjectCodec) anyCodec); - } + Utf8WriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_WRITER); return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs, anyCodec); } @SuppressWarnings("unchecked") private StringWriterCodec newUnwrappedStringWriter( - ObjectCodec owner, Class generatedClass) { + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { JsonFieldInfo[] fields = unwrappedWriteFields(owner); StringWriterCodec[] codecs = (StringWriterCodec[]) new StringWriterCodec[fields.length]; @@ -870,13 +776,7 @@ private StringWriterCodec newUnwrappedStringWriter( JsonFieldInfo field = fields[i]; if (JsonCodegen.usesWriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); - StringWriterCodec codec = child.stringWriter(); - if (JsonCodegen.writeNestedType(field) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = stringWriter((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.STRING_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -887,33 +787,25 @@ private StringWriterCodec newUnwrappedStringWriter( return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs); } - StringWriterCodec anyCodec = any.valueTypeInfo().stringWriter(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = stringWriter((ObjectCodec) anyCodec); - } + StringWriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.STRING_WRITER); return GeneratedCodecInstantiator.instantiateAnyStringWriter( generatedClass, owner, fields, codecs, anyCodec); } @SuppressWarnings("unchecked") private Utf8WriterCodec newUnwrappedUtf8Writer( - ObjectCodec owner, Class generatedClass) { + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { JsonFieldInfo[] fields = unwrappedWriteFields(owner); Utf8WriterCodec[] codecs = (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field)) { + if (JsonCodegen.usesUtf8WriteCodec(field, this)) { JsonTypeInfo child = field.writeTypeInfo(); - Utf8WriterCodec codec = child.utf8Writer(); - if (JsonCodegen.writeNestedType(field) != null - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf8Writer((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_WRITER); } } AnyInfo any = owner.anyInfo(); @@ -924,12 +816,8 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs); } - Utf8WriterCodec anyCodec = any.valueTypeInfo().utf8Writer(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Writer((ObjectCodec) anyCodec); - } + Utf8WriterCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_WRITER); return GeneratedCodecInstantiator.instantiateAnyUtf8Writer( generatedClass, owner, fields, codecs, anyCodec); } @@ -939,15 +827,22 @@ private static JsonFieldInfo[] unwrappedWriteFields(ObjectCodec owner) { } @SuppressWarnings("unchecked") - private Latin1ReaderCodec newLatin1Reader(ObjectCodec owner, Class generatedClass) { - return newLatin1Reader(owner, generatedClass, owner.readTable()); + private Latin1ReaderCodec newLatin1Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { + return newLatin1Reader(owner, generatedClass, owner.readTable(), capabilities, null); } @SuppressWarnings("unchecked") private Latin1ReaderCodec newLatin1Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Latin1ReaderCodec selfReader) { if (owner.unwrappedInfo() != null) { - return newUnwrappedLatin1Reader(owner, generatedClass, readTable); + return newUnwrappedLatin1Reader(owner, generatedClass, readTable, capabilities, selfReader); } JsonFieldInfo[] fields = owner.readFields(); JsonCreatorInfo creator = owner.creatorInfo(); @@ -956,7 +851,9 @@ private Latin1ReaderCodec newLatin1Reader( Latin1ReaderCodec[] codecs = (Latin1ReaderCodec[]) new Latin1ReaderCodec[creatorFields.length]; for (int i = 0; i < creatorFields.length; i++) { - codecs[i] = creatorFields[i].typeInfo().latin1Reader(); + codecs[i] = + resolvedCapability( + creatorFields[i].typeInfo(), capabilities, CapabilityKind.LATIN1_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -965,28 +862,23 @@ private Latin1ReaderCodec newLatin1Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs); - } - Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = latin1Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Latin1ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.LATIN1_READER); return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } Latin1ReaderCodec[] codecs = (Latin1ReaderCodec[]) new Latin1ReaderCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field)) { - codecs[i] = typeInfo.latin1Reader(); - } else if (JsonCodegen.readNestedType(field) != null && field.readRawType() != owner.type()) { - Latin1ReaderCodec codec = typeInfo.latin1Reader(); - codecs[i] = - codec instanceof ObjectCodec ? latin1Reader((ObjectCodec) codec) : codec; + if (JsonCodegen.usesReadCodec(field, this)) { + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); + } else if (JsonCodegen.readNestedType(field, this) != null + && field.readRawType() != owner.type()) { + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); } } AnyInfo any = owner.anyInfo(); @@ -996,28 +888,31 @@ private Latin1ReaderCodec newLatin1Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs); - } - Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = latin1Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Latin1ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.LATIN1_READER); return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") - private Utf16ReaderCodec newUtf16Reader(ObjectCodec owner, Class generatedClass) { - return newUtf16Reader(owner, generatedClass, owner.readTable()); + private Utf16ReaderCodec newUtf16Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { + return newUtf16Reader(owner, generatedClass, owner.readTable(), capabilities, null); } @SuppressWarnings("unchecked") private Utf16ReaderCodec newUtf16Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf16ReaderCodec selfReader) { if (owner.unwrappedInfo() != null) { - return newUnwrappedUtf16Reader(owner, generatedClass, readTable); + return newUnwrappedUtf16Reader(owner, generatedClass, readTable, capabilities, selfReader); } JsonFieldInfo[] fields = owner.readFields(); JsonCreatorInfo creator = owner.creatorInfo(); @@ -1026,7 +921,9 @@ private Utf16ReaderCodec newUtf16Reader( Utf16ReaderCodec[] codecs = (Utf16ReaderCodec[]) new Utf16ReaderCodec[creatorFields.length]; for (int i = 0; i < creatorFields.length; i++) { - codecs[i] = creatorFields[i].typeInfo().utf16Reader(); + codecs[i] = + resolvedCapability( + creatorFields[i].typeInfo(), capabilities, CapabilityKind.UTF16_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1035,27 +932,23 @@ private Utf16ReaderCodec newUtf16Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf16Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf16ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF16_READER); return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } Utf16ReaderCodec[] codecs = (Utf16ReaderCodec[]) new Utf16ReaderCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field)) { - codecs[i] = typeInfo.utf16Reader(); - } else if (JsonCodegen.readNestedType(field) != null && field.readRawType() != owner.type()) { - Utf16ReaderCodec codec = typeInfo.utf16Reader(); - codecs[i] = codec instanceof ObjectCodec ? utf16Reader((ObjectCodec) codec) : codec; + if (JsonCodegen.usesReadCodec(field, this)) { + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); + } else if (JsonCodegen.readNestedType(field, this) != null + && field.readRawType() != owner.type()) { + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); } } AnyInfo any = owner.anyInfo(); @@ -1065,28 +958,31 @@ private Utf16ReaderCodec newUtf16Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf16Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf16ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF16_READER); return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") - private Utf8ReaderCodec newUtf8Reader(ObjectCodec owner, Class generatedClass) { - return newUtf8Reader(owner, generatedClass, owner.readTable()); + private Utf8ReaderCodec newUtf8Reader( + ObjectCodec owner, + Class generatedClass, + IdentityMap capabilities) { + return newUtf8Reader(owner, generatedClass, owner.readTable(), capabilities, null); } @SuppressWarnings("unchecked") private Utf8ReaderCodec newUtf8Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf8ReaderCodec selfReader) { if (owner.unwrappedInfo() != null) { - return newUnwrappedUtf8Reader(owner, generatedClass, readTable); + return newUnwrappedUtf8Reader(owner, generatedClass, readTable, capabilities, selfReader); } JsonFieldInfo[] fields = owner.readFields(); JsonCreatorInfo creator = owner.creatorInfo(); @@ -1095,7 +991,9 @@ private Utf8ReaderCodec newUtf8Reader( Utf8ReaderCodec[] codecs = (Utf8ReaderCodec[]) new Utf8ReaderCodec[creatorFields.length]; for (int i = 0; i < creatorFields.length; i++) { - codecs[i] = creatorFields[i].typeInfo().utf8Reader(); + codecs[i] = + resolvedCapability( + creatorFields[i].typeInfo(), capabilities, CapabilityKind.UTF8_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1104,27 +1002,23 @@ private Utf8ReaderCodec newUtf8Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf8ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_READER); return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } Utf8ReaderCodec[] codecs = (Utf8ReaderCodec[]) new Utf8ReaderCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field)) { - codecs[i] = typeInfo.utf8Reader(); - } else if (JsonCodegen.readNestedType(field) != null && field.readRawType() != owner.type()) { - Utf8ReaderCodec codec = typeInfo.utf8Reader(); - codecs[i] = codec instanceof ObjectCodec ? utf8Reader((ObjectCodec) codec) : codec; + if (JsonCodegen.usesReadCodec(field, this)) { + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); + } else if (JsonCodegen.readNestedType(field, this) != null + && field.readRawType() != owner.type()) { + codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); } } AnyInfo any = owner.anyInfo(); @@ -1134,34 +1028,28 @@ private Utf8ReaderCodec newUtf8Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf8ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_READER); return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") private Latin1ReaderCodec newUnwrappedLatin1Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Latin1ReaderCodec selfReader) { JsonFieldInfo[] fields = unwrappedReadFields(owner); JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); Latin1ReaderCodec[] codecs = (Latin1ReaderCodec[]) new Latin1ReaderCodec[children.length]; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - Latin1ReaderCodec codec = child.latin1Reader(); - if (child.usesDefaultObjectCodec() - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = latin1Reader((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.LATIN1_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1170,34 +1058,28 @@ private Latin1ReaderCodec newUnwrappedLatin1Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs); - } - Latin1ReaderCodec anyCodec = any.valueTypeInfo().latin1Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = latin1Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Latin1ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.LATIN1_READER); return GeneratedCodecInstantiator.instantiateAnyLatin1Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") private Utf16ReaderCodec newUnwrappedUtf16Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf16ReaderCodec selfReader) { JsonFieldInfo[] fields = unwrappedReadFields(owner); JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); Utf16ReaderCodec[] codecs = (Utf16ReaderCodec[]) new Utf16ReaderCodec[children.length]; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - Utf16ReaderCodec codec = child.utf16Reader(); - if (child.usesDefaultObjectCodec() - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf16Reader((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF16_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1206,34 +1088,28 @@ private Utf16ReaderCodec newUnwrappedUtf16Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf16ReaderCodec anyCodec = any.valueTypeInfo().utf16Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf16Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf16ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF16_READER); return GeneratedCodecInstantiator.instantiateAnyUtf16Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } @SuppressWarnings("unchecked") private Utf8ReaderCodec newUnwrappedUtf8Reader( - ObjectCodec owner, Class generatedClass, JsonFieldTable readTable) { + ObjectCodec owner, + Class generatedClass, + JsonFieldTable readTable, + IdentityMap capabilities, + Utf8ReaderCodec selfReader) { JsonFieldInfo[] fields = unwrappedReadFields(owner); JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); Utf8ReaderCodec[] codecs = (Utf8ReaderCodec[]) new Utf8ReaderCodec[children.length]; for (int i = 0; i < children.length; i++) { JsonTypeInfo child = children[i]; - Utf8ReaderCodec codec = child.utf8Reader(); - if (child.usesDefaultObjectCodec() - && child.rawType() != owner.type() - && codec instanceof ObjectCodec) { - codec = utf8Reader((ObjectCodec) codec); - } - codecs[i] = codec; + codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_READER); } AnyInfo any = owner.anyInfo(); if (any == null || any.readField() == null && any.readSetter() == null) { @@ -1242,16 +1118,12 @@ private Utf8ReaderCodec newUnwrappedUtf8Reader( } if (!storesAnyCodec(owner, any)) { return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs); - } - Utf8ReaderCodec anyCodec = any.valueTypeInfo().utf8Reader(); - if (any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && anyCodec instanceof ObjectCodec) { - anyCodec = utf8Reader((ObjectCodec) anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader); } + Utf8ReaderCodec anyCodec = + resolvedCapability(any.valueTypeInfo(), capabilities, CapabilityKind.UTF8_READER); return GeneratedCodecInstantiator.instantiateAnyUtf8Reader( - generatedClass, owner, readTable, fields, codecs, anyCodec); + generatedClass, owner, readTable, fields, codecs, selfReader, anyCodec); } private static JsonFieldInfo[] unwrappedReadFields(ObjectCodec owner) { @@ -1290,501 +1162,705 @@ private static JsonTypeInfo[] unwrappedReadTypeInfos(ObjectCodec owner) { return children; } - private Latin1ReaderCodec newInlineLatin1Reader( - ObjectCodec owner, - Class generatedClass, - JsonFieldTable readTable, - Latin1ReaderCodec ordinaryReader) { - Latin1ReaderCodec codec = newLatin1Reader(owner, generatedClass, readTable); - setInlineSelfReader(owner, codec, ordinaryReader); - Field[] childFields = readerChildFields(codec, owner); - registerLatin1ReaderCallbacks(codec, owner, childFields); - registerLatin1AnyReaderCallback(codec, owner); - return codec; + private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { + return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); } - private Utf16ReaderCodec newInlineUtf16Reader( - ObjectCodec owner, - Class generatedClass, - JsonFieldTable readTable, - Utf16ReaderCodec ordinaryReader) { - Utf16ReaderCodec codec = newUtf16Reader(owner, generatedClass, readTable); - setInlineSelfReader(owner, codec, ordinaryReader); - Field[] childFields = readerChildFields(codec, owner); - registerUtf16ReaderCallbacks(codec, owner, childFields); - registerUtf16AnyReaderCallback(codec, owner); - return codec; + private enum CapabilityKind { + STRING_WRITER, + UTF8_WRITER, + LATIN1_READER, + UTF16_READER, + UTF8_READER } - private Utf8ReaderCodec newInlineUtf8Reader( - ObjectCodec owner, - Class generatedClass, - JsonFieldTable readTable, - Utf8ReaderCodec ordinaryReader) { - Utf8ReaderCodec codec = newUtf8Reader(owner, generatedClass, readTable); - setInlineSelfReader(owner, codec, ordinaryReader); - Field[] childFields = readerChildFields(codec, owner); - registerUtf8ReaderCallbacks(codec, owner, childFields); - registerUtf8AnyReaderCallback(codec, owner); - return codec; + private ArrayList capabilityChildren(ObjectCodec owner, CapabilityKind kind) { + ArrayList children = new ArrayList<>(); + AnyInfo any = owner.anyInfo(); + boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; + if (writer) { + JsonFieldInfo[] fields = + owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); + for (int i = 0; i < fields.length; i++) { + JsonFieldInfo field = fields[i]; + boolean usesCodec = + kind == CapabilityKind.UTF8_WRITER + ? JsonCodegen.usesUtf8WriteCodec(field, this) + : JsonCodegen.usesWriteCodec(field); + if (usesCodec + && (field.writeRawType() != owner.type() + || canonicalObjectOwner(field.writeTypeInfo()) == null)) { + children.add(field.writeTypeInfo()); + } + } + if (any != null + && (any.writeField() != null || any.writeGetter() != null) + && storesAnyCodec(owner, any)) { + children.add(any.valueTypeInfo()); + } + return children; + } + if (owner.unwrappedInfo() != null) { + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + JsonFieldInfo[] fields = owner.readFields(); + for (int i = 0; i < fields.length; i++) { + addReadDependency(children, owner, fields[i]); + } + } else { + JsonCreatorFieldInfo[] fields = creator.fields(); + for (int i = 0; i < fields.length; i++) { + children.add(fields[i].typeInfo()); + } + } + JsonUnwrappedInfo.ReadRoute[] routes = owner.unwrappedInfo().readRoutes(); + for (int i = 0; i < routes.length; i++) { + JsonUnwrappedInfo.ReadRoute route = routes[i]; + if (route.field() == null) { + children.add(route.creatorField().typeInfo()); + } else { + addReadDependency(children, owner, route.field()); + } + } + } else if (owner.creatorInfo() == null) { + JsonFieldInfo[] fields = owner.readFields(); + for (int i = 0; i < fields.length; i++) { + addReadDependency(children, owner, fields[i]); + } + } else { + JsonCreatorFieldInfo[] fields = owner.creatorInfo().fields(); + for (int i = 0; i < fields.length; i++) { + children.add(fields[i].typeInfo()); + } + } + if (any != null + && (any.readField() != null || any.readSetter() != null) + && storesAnyCodec(owner, any)) { + children.add(any.valueTypeInfo()); + } + return children; } - private static void setInlineSelfReader( - ObjectCodec owner, T inlineReader, T ordinaryReader) { - if (JsonCodegen.storesSelfReader(owner)) { - Field field = ReflectionUtils.getField(inlineReader.getClass(), "selfReader"); - ReflectionUtils.setObjectFieldValue(inlineReader, field, ordinaryReader); + private void addReadDependency( + ArrayList children, ObjectCodec owner, JsonFieldInfo field) { + if (JsonCodegen.usesReadCodec(field, this) + || JsonCodegen.readNestedType(field, this) != null && field.readRawType() != owner.type()) { + children.add(field.readTypeInfo()); } } - private static boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { - return !any.valueTypeInfo().usesDefaultObjectCodec() || any.valueRawType() != owner.type(); + /** Returns whether a generated writer must traverse this cyclic edge through its type slot. */ + @Internal + public boolean usesWriterSlot(Class ownerType, JsonTypeInfo child) { + jitContext.lock(); + try { + JsonTypeInfo owner = rawObjectTypeInfos.get(ownerType); + return owner != null + && child != owner + && canonicalObjectOwner(child) != null + && reachesWriter(child, owner, new IdentityMap<>()); + } finally { + jitContext.unlock(); + } } - private Field[] writerChildFields(Object parent, ObjectCodec owner) { - Field[] childFields = null; - JsonFieldInfo[] fields = - owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); - for (int i = 0; i < fields.length; i++) { - Class nestedType = JsonCodegen.writeNestedType(fields[i]); - JsonTypeInfo child = fields[i].writeTypeInfo(); - if (nestedType != null - && nestedType != owner.type() - && rawObjectTypeInfos.get(child.rawType()) == child) { - if (childFields == null) { - childFields = new Field[fields.length]; - } - childFields[i] = ReflectionUtils.getField(parent.getClass(), "w" + i); - } + /** Returns whether a generated reader must traverse this cyclic edge through its type slot. */ + @Internal + public boolean usesReaderSlot(Class ownerType, JsonTypeInfo child) { + jitContext.lock(); + try { + JsonTypeInfo ownerInfo = rawObjectTypeInfos.get(ownerType); + ObjectCodec owner = ownerInfo == null ? null : canonicalObjectOwner(ownerInfo); + return owner != null + && owner.unwrappedInfo() == null + && child != ownerInfo + && canonicalObjectOwner(child) != null + && reachesReader(child, ownerInfo, new IdentityMap<>()); + } finally { + jitContext.unlock(); } - return childFields; } - private Field[] readerChildFields(Object parent, ObjectCodec owner) { - if (owner.unwrappedInfo() != null) { - return unwrappedReaderChildFields(parent, owner); + private boolean reachesWriter( + JsonTypeInfo current, JsonTypeInfo target, IdentityMap visited) { + if (current == target) { + return true; } - Field[] childFields = null; - JsonCreatorInfo creator = owner.creatorInfo(); - if (creator != null) { - JsonCreatorFieldInfo[] fields = creator.fields(); - for (int i = 0; i < fields.length; i++) { - JsonTypeInfo child = fields[i].typeInfo(); - if (child.usesDefaultObjectCodec() - && child.rawType() != owner.type() - && rawObjectTypeInfos.get(child.rawType()) == child) { - if (childFields == null) { - childFields = new Field[fields.length]; - } - childFields[i] = ReflectionUtils.getField(parent.getClass(), "r" + i); - } - } - return childFields; + if (visited.put(current, Boolean.TRUE) != null) { + return false; } - JsonFieldInfo[] fields = owner.readFields(); - for (int i = 0; i < fields.length; i++) { - Class nestedType = JsonCodegen.readNestedType(fields[i]); - JsonTypeInfo child = fields[i].readTypeInfo(); - if (nestedType != null - && nestedType != owner.type() - && rawObjectTypeInfos.get(child.rawType()) == child) { - if (childFields == null) { - childFields = new Field[fields.length]; + ObjectCodec owner = canonicalObjectOwner(current); + if (owner != null) { + ArrayList children = capabilityChildren(owner, CapabilityKind.STRING_WRITER); + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + if (child == target + || canonicalObjectOwner(child) != null && reachesWriter(child, target, visited)) { + return true; + } + Object capability = child.stringWriter(); + if (capability instanceof ClosedSubtypeCodec + && reachesWriter((ClosedSubtypeCodec) capability, target, visited)) { + return true; } - childFields[i] = ReflectionUtils.getField(parent.getClass(), "o" + i); } } - return childFields; + return false; } - private Field[] unwrappedReaderChildFields(Object parent, ObjectCodec owner) { - JsonTypeInfo[] children = unwrappedReadTypeInfos(owner); - JsonCreatorInfo creator = owner.creatorInfo(); - int directCount = creator == null ? owner.readFields().length : creator.fields().length; - JsonUnwrappedInfo.ReadRoute[] routes = owner.unwrappedInfo().readRoutes(); - Field[] childFields = null; - for (int i = 0; i < children.length; i++) { - JsonTypeInfo child = children[i]; - if (!child.usesDefaultObjectCodec() - || child.rawType() == owner.type() - || rawObjectTypeInfos.get(child.rawType()) != child) { - continue; + private boolean reachesWriter( + ClosedSubtypeCodec subtype, JsonTypeInfo target, IdentityMap visited) { + for (int i = 0; i < subtype.childCount(); i++) { + JsonTypeInfo child = subtype.child(i); + if (child == target || reachesWriter(child, target, visited)) { + return true; } - String fieldName; - if (i < directCount) { - if (creator == null && JsonCodegen.readNestedType(owner.readFields()[i]) == null) { - continue; + } + return false; + } + + private boolean reachesReader( + JsonTypeInfo current, JsonTypeInfo target, IdentityMap visited) { + if (current == target) { + return true; + } + if (visited.put(current, Boolean.TRUE) != null) { + return false; + } + ObjectCodec owner = canonicalObjectOwner(current); + if (owner != null && owner.unwrappedInfo() == null) { + ArrayList children = capabilityChildren(owner, CapabilityKind.UTF8_READER); + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + if (child == target + || canonicalObjectOwner(child) != null && reachesReader(child, target, visited)) { + return true; } - fieldName = creator == null ? "o" + i : "r" + i; - } else { - JsonUnwrappedInfo.ReadRoute route = routes[i - directCount]; - if (route.field() != null && JsonCodegen.readNestedType(route.field()) == null) { - continue; + Object capability = child.utf8Reader(); + if (capability instanceof ClosedSubtypeCodec + && reachesReader((ClosedSubtypeCodec) capability, target, visited)) { + return true; } - fieldName = (route.field() == null ? "r" : "o") + i; } - if (childFields == null) { - childFields = new Field[children.length]; - } - childFields[i] = ReflectionUtils.getField(parent.getClass(), fieldName); - } - return childFields; - } - - // Publication runs under the local JIT lock: construct the resolver-local instance, resolve - // every replaceable child Field, register child notifications, then write the canonical - // JsonTypeInfo slot captured when the compilation request is created. Capturing that exact slot - // is required because a failed closed-subtype transaction can remove its provisional metadata - // and later build a new canonical slot for the same class before an old async task completes. - // The old task may refine only its now-unreachable slot; a type lookup here would let it corrupt - // the replacement generation. Construction and field lookup are the fallible phase. Publication - // is deterministic ordinary field assignment and is never modeled as a transaction or rolled - // back; a failure there is a generated-code invariant violation. - private void publishStringWriter( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - StringWriterCodec codec = newStringWriter(owner, generated); - Field[] childFields = writerChildFields(codec, owner); - registerStringWriterCallbacks(codec, owner, childFields); - registerStringAnyWriterCallback(codec, owner); - typeInfo.setStringWriter(codec); + } + return false; } - private void publishUtf8Writer( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Utf8WriterCodec codec = newUtf8Writer(owner, generated); - Field[] childFields = writerChildFields(codec, owner); - registerUtf8WriterCallbacks(codec, owner, childFields); - registerUtf8AnyWriterCallback(codec, owner); - typeInfo.setUtf8Writer(codec); + private boolean reachesReader( + ClosedSubtypeCodec subtype, JsonTypeInfo target, IdentityMap visited) { + for (int i = 0; i < subtype.childCount(); i++) { + JsonTypeInfo child = subtype.child(i); + if (child == target || reachesReader(child, target, visited)) { + return true; + } + } + return false; } - private void publishLatin1Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Latin1ReaderCodec codec = newLatin1Reader(owner, generated); - Field[] childFields = readerChildFields(codec, owner); - registerLatin1ReaderCallbacks(codec, owner, childFields); - registerLatin1AnyReaderCallback(codec, owner); - typeInfo.setLatin1Reader(codec); + private boolean canCompile(ObjectCodec owner, CapabilityKind kind) { + return kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER + ? codegen.canCompileWriter(owner) + : codegen.canCompileReader(owner); } - private void publishUtf16Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Utf16ReaderCodec codec = newUtf16Reader(owner, generated); - Field[] childFields = readerChildFields(codec, owner); - registerUtf16ReaderCallbacks(codec, owner, childFields); - registerUtf16AnyReaderCallback(codec, owner); - typeInfo.setUtf16Reader(codec); + private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + return typeInfo.stringWriter(); + case UTF8_WRITER: + return typeInfo.utf8Writer(); + case LATIN1_READER: + return typeInfo.latin1Reader(); + case UTF16_READER: + return typeInfo.utf16Reader(); + case UTF8_READER: + return typeInfo.utf8Reader(); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } } - private void publishUtf8Reader( - ObjectCodec owner, JsonTypeInfo typeInfo, Class generated) { - requireJITLock(); - Utf8ReaderCodec codec = newUtf8Reader(owner, generated); - Field[] childFields = readerChildFields(codec, owner); - registerUtf8ReaderCallbacks(codec, owner, childFields); - registerUtf8AnyReaderCallback(codec, owner); - typeInfo.setUtf8Reader(codec); - } - - // A generated parent captures the current child slot during construction. If a child task is - // active, notification updates only the matching concrete field after the child slot is - // published. If no task is active, onNotifyMissed installs the already-current slot immediately. - // The callback list is notification state, not a resolver dependency graph or task-dedup map. - private void registerStringWriterCallbacks( - StringWriterCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; + private CompletableFuture> generatedClass(CapabilityNode node, CapabilityKind kind) { + if (node.subtypeOwner != null) { + throw new IllegalStateException("Inline subtype readers reuse child generated classes"); } - JsonFieldInfo[] properties = - owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = properties[i].writeTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.stringWriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - StringWriterCodec codec = child.stringWriter(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.stringWriter()); - } - }); + if (node.collectionOwner != null) { + if (kind == CapabilityKind.UTF8_WRITER) { + return sharedRegistry.utf8CollectionWriterClass(node.typeInfo.type(), node.collectionOwner); + } + if (kind == CapabilityKind.UTF8_READER) { + return sharedRegistry.utf8CollectionReaderClass(node.typeInfo.type(), node.collectionOwner); + } + throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); + } + switch (kind) { + case STRING_WRITER: + return sharedRegistry.stringWriterClass(node.objectOwner, this); + case UTF8_WRITER: + return sharedRegistry.utf8WriterClass(node.objectOwner, this); + case LATIN1_READER: + return sharedRegistry.latin1ReaderClass(node.objectOwner, this); + case UTF16_READER: + return sharedRegistry.utf16ReaderClass(node.objectOwner, this); + case UTF8_READER: + return sharedRegistry.utf8ReaderClass(node.objectOwner, this); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } + } + + private Object newCapability( + CapabilityNode node, + Class generatedClass, + IdentityMap capabilities, + CapabilityKind kind) { + if (node.subtypeOwner != null) { + return newSubtypeReaders(node.subtypeOwner, capabilities, kind); + } + if (node.collectionOwner != null) { + JsonTypeInfo element = declaredCollectionElement(node.typeInfo); + if (kind == CapabilityKind.UTF8_WRITER) { + Utf8WriterCodec elementWriter = resolvedCapability(element, capabilities, kind); + Utf8WriterCodec fallback = eraseUtf8Writer(node.collectionOwner); + if (node.collectionOwner instanceof CollectionCodec.StringCollectionCodec) { + return GeneratedCodecInstantiator.instantiateUtf8CollectionWriter( + generatedClass, fallback); + } + return GeneratedCodecInstantiator.instantiateUtf8CollectionWriter( + generatedClass, fallback, elementWriter); + } + if (kind == CapabilityKind.UTF8_READER) { + Utf8ReaderCodec elementReader = resolvedCapability(element, capabilities, kind); + return GeneratedCodecInstantiator.instantiateUtf8CollectionReader( + generatedClass, elementReader); } + throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); + } + switch (kind) { + case STRING_WRITER: + return newStringWriter(node.objectOwner, generatedClass, capabilities); + case UTF8_WRITER: + return newUtf8Writer(node.objectOwner, generatedClass, capabilities); + case LATIN1_READER: + return newLatin1Reader(node.objectOwner, generatedClass, capabilities); + case UTF16_READER: + return newUtf16Reader(node.objectOwner, generatedClass, capabilities); + case UTF8_READER: + return newUtf8Reader(node.objectOwner, generatedClass, capabilities); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); } } - private void registerUtf8WriterCallbacks( - Utf8WriterCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; - } - JsonFieldInfo[] properties = - owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = properties[i].writeTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf8WriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8WriterCodec codec = child.utf8Writer(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Writer()); - } - }); - } + @SuppressWarnings("unchecked") + private static T resolvedCapability( + JsonTypeInfo typeInfo, IdentityMap capabilities, CapabilityKind kind) { + Object capability = capabilities.get(typeInfo); + if (capability != null) { + return (T) capability; } + return (T) currentCapability(typeInfo, kind); } - private void registerLatin1ReaderCallbacks( - Latin1ReaderCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; + @SuppressWarnings("unchecked") + private static Utf8WriterCodec eraseUtf8Writer(CollectionCodec codec) { + return (Utf8WriterCodec) (Utf8WriterCodec) codec; + } + + private static void installCapability( + JsonTypeInfo typeInfo, Object capability, CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + typeInfo.setStringWriter((StringWriterCodec) capability); + return; + case UTF8_WRITER: + typeInfo.setUtf8Writer((Utf8WriterCodec) capability); + return; + case LATIN1_READER: + typeInfo.setLatin1Reader((Latin1ReaderCodec) capability); + return; + case UTF16_READER: + typeInfo.setUtf16Reader((Utf16ReaderCodec) capability); + return; + case UTF8_READER: + typeInfo.setUtf8Reader((Utf8ReaderCodec) capability); + return; + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); } - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = readerChildTypeInfo(owner, i); - jitContext.registerJITNotifyCallback( - codegen.latin1ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Latin1ReaderCodec codec = child.latin1Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.latin1Reader()); - } - }); - } + } + + @SuppressWarnings("unchecked") + private Object newSubtypeReaders( + ClosedSubtypeCodec subtype, + IdentityMap capabilities, + CapabilityKind kind) { + int childCount = subtype.childCount(); + switch (kind) { + case LATIN1_READER: + Latin1ReaderCodec[] latin1Readers = + (Latin1ReaderCodec[]) new Latin1ReaderCodec[childCount]; + for (int i = 0; i < childCount; i++) { + JsonFieldTable table = subtype.inlineReadTable(i); + if (table != null) { + JsonTypeInfo child = subtype.child(i); + ObjectCodec owner = erase(requireObjectOwner(child)); + Latin1ReaderCodec canonical = resolvedCapability(child, capabilities, kind); + latin1Readers[i] = + newLatin1Reader(owner, canonical.getClass(), table, capabilities, canonical); + } + } + return latin1Readers; + case UTF16_READER: + Utf16ReaderCodec[] utf16Readers = + (Utf16ReaderCodec[]) new Utf16ReaderCodec[childCount]; + for (int i = 0; i < childCount; i++) { + JsonFieldTable table = subtype.inlineReadTable(i); + if (table != null) { + JsonTypeInfo child = subtype.child(i); + ObjectCodec owner = erase(requireObjectOwner(child)); + Utf16ReaderCodec canonical = resolvedCapability(child, capabilities, kind); + utf16Readers[i] = + newUtf16Reader(owner, canonical.getClass(), table, capabilities, canonical); + } + } + return utf16Readers; + case UTF8_READER: + Utf8ReaderCodec[] utf8Readers = + (Utf8ReaderCodec[]) new Utf8ReaderCodec[childCount]; + for (int i = 0; i < childCount; i++) { + JsonFieldTable table = subtype.inlineReadTable(i); + if (table != null) { + JsonTypeInfo child = subtype.child(i); + ObjectCodec owner = erase(requireObjectOwner(child)); + Utf8ReaderCodec canonical = resolvedCapability(child, capabilities, kind); + utf8Readers[i] = + newUtf8Reader(owner, canonical.getClass(), table, capabilities, canonical); + } + } + return utf8Readers; + default: + throw new IllegalStateException("Writer graph cannot construct inline subtype readers"); } } - private void registerUtf16ReaderCallbacks( - Utf16ReaderCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; + private ObjectCodec requireObjectOwner(JsonTypeInfo typeInfo) { + ObjectCodec owner = canonicalObjectOwner(typeInfo); + if (owner == null) { + throw new IllegalStateException( + "Inline subtype lost its canonical object owner: " + typeInfo.rawType().getName()); } - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = readerChildTypeInfo(owner, i); - jitContext.registerJITNotifyCallback( - codegen.utf16ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf16ReaderCodec codec = child.utf16Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf16Reader()); - } - }); + return owner; + } + + private static boolean readerKind(CapabilityKind kind) { + return kind == CapabilityKind.LATIN1_READER + || kind == CapabilityKind.UTF16_READER + || kind == CapabilityKind.UTF8_READER; + } + + private static boolean hasInlineReadTable(ClosedSubtypeCodec subtype) { + for (int i = 0; i < subtype.childCount(); i++) { + if (subtype.inlineReadTable(i) != null) { + return true; } } + return false; } - private void registerUtf8ReaderCallbacks( - Utf8ReaderCodec parent, ObjectCodec owner, Field[] fields) { - if (fields == null) { - return; + private static Object currentSubtypeReaders(ClosedSubtypeCodec subtype, CapabilityKind kind) { + switch (kind) { + case LATIN1_READER: + return subtype.inlineLatin1Readers(); + case UTF16_READER: + return subtype.inlineUtf16Readers(); + case UTF8_READER: + return subtype.inlineUtf8Readers(); + default: + throw new IllegalStateException("Writer graph has no inline subtype readers"); } - for (int i = 0; i < fields.length; i++) { - Field field = fields[i]; - if (field != null) { - JsonTypeInfo child = readerChildTypeInfo(owner, i); - jitContext.registerJITNotifyCallback( - codegen.utf8ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8ReaderCodec codec = child.utf8Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Reader()); - } - }); + } + + @SuppressWarnings("unchecked") + private static void installSubtypeReaders( + ClosedSubtypeCodec subtype, Object readers, CapabilityKind kind) { + switch (kind) { + case LATIN1_READER: + subtype.installInlineLatin1Readers((Latin1ReaderCodec[]) readers); + return; + case UTF16_READER: + subtype.installInlineUtf16Readers((Utf16ReaderCodec[]) readers); + return; + case UTF8_READER: + subtype.installInlineUtf8Readers((Utf8ReaderCodec[]) readers); + return; + default: + throw new IllegalStateException("Writer graph has no inline subtype readers"); + } + } + + private void requestCapabilities(ArrayList roots) { + for (CapabilityKind kind : CapabilityKind.values()) { + CapabilityGraph graph = new CapabilityGraph(kind); + if (graph.addRoots(roots) && !graph.ordered.isEmpty()) { + requestGraph(graph); } } } - private void registerStringAnyWriterCallback( - StringWriterCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyWriteChild(owner, any)) { - return; - } - Field field = ReflectionUtils.getField(parent.getClass(), "anyWriter"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.stringWriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { + private void requestGraph(CapabilityGraph graph) { + jitContext.registerJITFuture( + () -> graph.classesReady().thenApply(ignored -> graph), + new JsonJITContext.JITCallback() { @Override - public void onNotifyResult(Object result) { - StringWriterCodec codec = child.stringWriter(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); + public void onSuccess(CapabilityGraph result) { + result.publish(); } @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.stringWriter()); + public void onFailure(Throwable failure) {} + + @Override + public Object id() { + return graph; } }); } - private void registerUtf8AnyWriterCallback(Utf8WriterCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyWriteChild(owner, any)) { - return; + /** + * One representation graph whose constructor dependencies are acyclic after canonical + * multi-object cycles become slot edges. Every class future is submitted before dependency order + * is applied to resolver-local instance construction and one lock-held publication loop. + */ + private final class CapabilityGraph { + private final CapabilityKind kind; + private final IdentityMap nodes = new IdentityMap<>(); + private final IdentityMap subtypes = new IdentityMap<>(); + private final ArrayList ordered = new ArrayList<>(); + + private CapabilityGraph(CapabilityKind kind) { + this.kind = kind; } - Field field = ReflectionUtils.getField(parent.getClass(), "anyWriter"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf8WriterJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8WriterCodec codec = child.utf8Writer(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Writer()); - } - }); - } + private boolean addRoots(ArrayList roots) { + for (int i = 0; i < roots.size(); i++) { + if (!addDependency(roots.get(i))) { + return false; + } + } + return true; + } - private void registerLatin1AnyReaderCallback( - Latin1ReaderCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyReadChild(owner, any)) { - return; + private boolean addDependency(JsonTypeInfo typeInfo) { + return addDependency(typeInfo, false); } - Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.latin1ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Latin1ReaderCodec codec = child.latin1Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.latin1Reader()); - } - }); - } + private boolean addDependency(JsonTypeInfo typeInfo, boolean slotEdge) { + ObjectCodec objectOwner = canonicalObjectOwner(typeInfo); + if (objectOwner != null) { + return addObject(objectOwner, typeInfo, slotEdge); + } + if (kind == CapabilityKind.UTF8_WRITER || kind == CapabilityKind.UTF8_READER) { + CollectionCodec collectionOwner = + kind == CapabilityKind.UTF8_WRITER + ? exactUtf8WriterCollectionOwner(typeInfo) + : exactUtf8CollectionOwner(typeInfo); + if (collectionOwner != null) { + return addCollection(collectionOwner, typeInfo); + } + } + Object capability = currentCapability(typeInfo, kind); + return !(capability instanceof ClosedSubtypeCodec) + || addSubtype(typeInfo, (ClosedSubtypeCodec) capability); + } - private void registerUtf16AnyReaderCallback( - Utf16ReaderCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyReadChild(owner, any)) { - return; + private boolean addSubtype(JsonTypeInfo typeInfo, ClosedSubtypeCodec subtype) { + Boolean complete = subtypes.get(subtype); + if (complete != null) { + return complete; + } + subtypes.put(subtype, Boolean.FALSE); + for (int i = 0; i < subtype.childCount(); i++) { + if (!addDependency(subtype.child(i))) { + return false; + } + } + subtypes.put(subtype, Boolean.TRUE); + if (readerKind(kind) && hasInlineReadTable(subtype)) { + Object initial = currentSubtypeReaders(subtype, kind); + if (initial == null) { + ordered.add(new CapabilityNode(typeInfo, subtype, initial)); + } + } + return true; } - Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf16ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf16ReaderCodec codec = child.utf16Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf16Reader()); - } - }); - } + private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo, boolean slotEdge) { + ObjectCodec owner = erase(rawOwner); + Object initial = currentCapability(typeInfo, kind); + if (initial != owner) { + return true; + } + CapabilityNode existing = nodes.get(typeInfo); + if (existing != null) { + return existing.complete || slotEdge; + } + if (!canCompile(owner, kind)) { + return false; + } + CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); + nodes.put(typeInfo, node); + ArrayList children = capabilityChildren(owner, kind); + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; + boolean childSlot = + writer ? usesWriterSlot(owner.type(), child) : usesReaderSlot(owner.type(), child); + if (!addDependency(child, childSlot)) { + return false; + } + } + node.complete = true; + ordered.add(node); + return true; + } - private void registerUtf8AnyReaderCallback(Utf8ReaderCodec parent, ObjectCodec owner) { - AnyInfo any = owner.anyInfo(); - if (!hasGeneratedAnyReadChild(owner, any)) { - return; + private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { + Object initial = currentCapability(typeInfo, kind); + if (initial != owner) { + return true; + } + CapabilityNode existing = nodes.get(typeInfo); + if (existing != null) { + return existing.complete; + } + JsonTypeInfo element = declaredCollectionElement(typeInfo); + if (element == null) { + return false; + } + CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); + nodes.put(typeInfo, node); + if (!addDependency(element)) { + return false; + } + node.complete = true; + ordered.add(node); + return true; } - Field field = ReflectionUtils.getField(parent.getClass(), "anyReader"); - JsonTypeInfo child = any.valueTypeInfo(); - jitContext.registerJITNotifyCallback( - codegen.utf8ReaderJITId(child.rawType()), - new JsonJITContext.NotifyCallback() { - @Override - public void onNotifyResult(Object result) { - Utf8ReaderCodec codec = child.utf8Reader(); - checkGeneratedClass(result, codec); - ReflectionUtils.setObjectFieldValue(parent, field, codec); - } - @Override - public void onNotifyMissed() { - ReflectionUtils.setObjectFieldValue(parent, field, child.utf8Reader()); + private CompletableFuture classesReady() { + ArrayList> futures = new ArrayList<>(); + for (int i = 0; i < ordered.size(); i++) { + CapabilityNode node = ordered.get(i); + if (node.subtypeOwner != null) { + continue; + } + node.classFuture = generatedClass(node, kind); + futures.add(node.classFuture); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + + private void publish() { + requireJITLock(); + IdentityMap capabilities = new IdentityMap<>(); + ArrayList unpublished = new ArrayList<>(); + for (int i = 0; i < ordered.size(); i++) { + CapabilityNode node = ordered.get(i); + if (!node.metadataMatches(kind)) { + return; + } + Object current = node.current(kind); + if (current != node.initial) { + if (node.subtypeOwner == null) { + capabilities.put(node.typeInfo, current); } - }); + continue; + } + Class generatedClass = null; + if (node.subtypeOwner == null) { + generatedClass = node.classFuture.getNow(null); + if (generatedClass == null) { + throw new IllegalStateException("Generated JSON class is not ready"); + } + } + node.instance = newCapability(node, generatedClass, capabilities, kind); + if (node.subtypeOwner == null) { + capabilities.put(node.typeInfo, node.instance); + } + unpublished.add(node); + } + for (int i = 0; i < unpublished.size(); i++) { + CapabilityNode node = unpublished.get(i); + if (!node.metadataMatches(kind) || node.current(kind) != node.initial) { + return; + } + } + for (int i = 0; i < unpublished.size(); i++) { + CapabilityNode node = unpublished.get(i); + node.install(kind); + } + } } - private boolean hasGeneratedAnyWriteChild(ObjectCodec owner, AnyInfo any) { - return any != null - && (any.writeField() != null || any.writeGetter() != null) - && any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && rawObjectTypeInfos.get(any.valueRawType()) == any.valueTypeInfo(); - } + private final class CapabilityNode { + private final JsonTypeInfo typeInfo; + private final ObjectCodec objectOwner; + private final CollectionCodec collectionOwner; + private final ClosedSubtypeCodec subtypeOwner; + private final Object initial; + private boolean complete; + private CompletableFuture> classFuture; + private Object instance; - private boolean hasGeneratedAnyReadChild(ObjectCodec owner, AnyInfo any) { - return any != null - && (any.readField() != null || any.readSetter() != null) - && any.valueTypeInfo().usesDefaultObjectCodec() - && any.valueRawType() != owner.type() - && rawObjectTypeInfos.get(any.valueRawType()) == any.valueTypeInfo(); - } + private CapabilityNode(JsonTypeInfo typeInfo, ObjectCodec owner, Object initial) { + this.typeInfo = typeInfo; + objectOwner = owner; + collectionOwner = null; + subtypeOwner = null; + this.initial = initial; + } - private static JsonTypeInfo readerChildTypeInfo(ObjectCodec owner, int index) { - if (owner.unwrappedInfo() != null) { - return unwrappedReadTypeInfos(owner)[index]; + private CapabilityNode(JsonTypeInfo typeInfo, CollectionCodec owner, Object initial) { + this.typeInfo = typeInfo; + objectOwner = null; + collectionOwner = owner; + subtypeOwner = null; + this.initial = initial; + } + + private CapabilityNode(JsonTypeInfo typeInfo, ClosedSubtypeCodec owner, Object initial) { + this.typeInfo = typeInfo; + objectOwner = null; + collectionOwner = null; + subtypeOwner = owner; + this.initial = initial; + } + + private boolean metadataMatches(CapabilityKind kind) { + if (subtypeOwner != null) { + return currentCapability(typeInfo, kind) == subtypeOwner; + } + if (objectOwner != null) { + return canonicalObjectOwner(typeInfo) == objectOwner; + } + return collectionCodecs.get(typeInfo) == collectionOwner + && typeInfos.get(typeInfoKey(typeInfo.type(), typeInfo.rawType())) == typeInfo; + } + + private Object current(CapabilityKind kind) { + return subtypeOwner == null + ? currentCapability(typeInfo, kind) + : currentSubtypeReaders(subtypeOwner, kind); + } + + private void install(CapabilityKind kind) { + if (subtypeOwner == null) { + installCapability(typeInfo, instance, kind); + } else { + installSubtypeReaders(subtypeOwner, instance, kind); + } } - JsonCreatorInfo creator = owner.creatorInfo(); - return creator == null - ? owner.readFields()[index].readTypeInfo() - : creator.fields()[index].typeInfo(); } @SuppressWarnings("unchecked") @@ -1836,7 +1912,7 @@ private JsonTypeInfo buildTypeInfo(Class rawType, Type declaredType, Object k } JsonTypeInfo typeInfo = newTypeInfo(declaredType, rawType, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerTypeInfoOwner(typeInfo, codec); return typeInfo; } @@ -1854,7 +1930,7 @@ private JsonTypeInfo buildObjectTypeInfo(TypeRef ownerType, Object key) { // capability slots. The outer cold-resolution transaction removes both on failure. objectCodecs.put(key, codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerTypeInfoOwner(typeInfo, codec); codec.resolveTypes(this); return typeInfo; } @@ -1862,27 +1938,10 @@ private JsonTypeInfo buildObjectTypeInfo(TypeRef ownerType, Object key) { // now; the outer owner finishes field resolution before returning the codec to its caller. typeInfo = newTypeInfo(ownerType.getType(), ownerType.getRawType(), codec); typeInfos.put(key, typeInfo); - registerObjectTypeInfo(typeInfo); + registerTypeInfoOwner(typeInfo, codec); return typeInfo; } - private JsonTypeInfo buildRuntimeTypeInfo(Class rawType) { - JsonTypeInfo custom = customTypeInfo(rawType, rawType); - if (custom != null) { - return custom; - } - sharedRegistry.checkSecure(rawType); - TypeRef typeRef = TypeRef.of(rawType); - JsonValueCodec codec = - rawType == Object.class - ? getObjectCodec(typeRef) - : sharedRegistry.createCodec(rawType, typeRef, this); - if (codec == null) { - codec = getObjectCodec(typeRef); - } - return newTypeInfo(rawType, rawType, codec); - } - private JsonTypeInfo newTypeInfo(Type type, Class rawType, JsonValueCodec codec) { return new JsonTypeInfo(type, rawType, sharedRegistry.kind(rawType), bindCodec(codec)); } @@ -1896,23 +1955,19 @@ private JsonTypeInfo newTypeInfo( return new JsonTypeInfo(type, rawType, kind, bindCodec(codec), annotationCodec); } - private void registerObjectTypeInfo(JsonTypeInfo typeInfo) { - if (typeInfo.usesDefaultObjectCodec() + private void registerTypeInfoOwner(JsonTypeInfo typeInfo, JsonValueCodec initialCodec) { + if (initialCodec instanceof CollectionCodec) { + collectionCodecs.put(typeInfo, (CollectionCodec) initialCodec); + } + if (initialCodec.getClass() == ObjectCodec.class && typeInfo.type() instanceof Class && typeInfo.rawType() != Object.class) { - ObjectCodec owner = (ObjectCodec) typeInfo.stringWriter(); + ObjectCodec owner = (ObjectCodec) initialCodec; rawObjectTypeInfos.put(typeInfo.rawType(), typeInfo); canonicalObjectTypeInfos.put(owner, typeInfo); } } - private static void checkGeneratedClass(Object result, Object codec) { - if (codec.getClass() != result) { - throw new IllegalStateException( - "Generated JSON callback does not match installed capability"); - } - } - private static Object typeInfoKey(Type declaredType, Class rawType) { return declaredType instanceof Class ? rawType : declaredType; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index ae6cfc11fe..b5ab0df76d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -170,16 +170,8 @@ public void writeInt(int value) { @Override public void writeLong(long value) { - if (value == Long.MIN_VALUE) { - writeRaw(MIN_LONG_BYTES); - return; - } ensure(20); - if (value < 0) { - buffer[position++] = (byte) '-'; - value = -value; - } - writePositiveLongNoEnsure(value); + writeLongNoEnsure(value); } @Override @@ -269,16 +261,108 @@ public void writeChar(char value) { writeByteRaw((byte) '"'); } + /** + * Writes one String through a deliberately independent UTF-8 representation boundary. + * + *

The compact-Latin1 common entry is intentionally large enough, through real representation + * dispatch, scanning, validation, and copying work, to exceed HotSpot JDK 25's 325-byte + * hot-inline limit. Generated object and collection code must call this method directly and + * account only for that invocation in its own group budget. If this method is reduced to a small + * wrapper around deeper helpers, C2 can absorb the wrapper and choose different transitive String + * closures according to compilation order. + * + *

Fixed-length Latin1 paths belong to this representation owner. When a measured layout keeps + * those paths directly in this method, repeated loads, predicates, and stores are intentional: + * extracting them merely to reduce source duplication can make C2's changing inline budget decide + * which String lengths pay another call. The local form also keeps {@code buffer}, {@code + * position}, and the cursor visible to C2. Keep escape, Unicode, malformed-input, and + * arbitrary-length work in their existing cold or long-tail methods; do not enlarge this method + * with cold behavior merely to preserve its size, and never use bytecode padding or compiler + * directives. + */ @Override public void writeString(String value) { if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - if (bytes.length == value.length()) { - if (writeLatin1String(bytes)) { - return; + byte[] stringBytes = StringSerializer.getStringBytes(value); + int length = stringBytes.length; + if (length == value.length()) { + ensure(length + 2); + int start = position; + // This representation owner contains the complete compact Latin1 common path. Generated + // schema and collection methods therefore call one natural, compilation-order-independent + // C2 boundary without duplicating String work. + if (length > 31) { + if (writeLongLatin1StringNoEnsure(stringBytes, length)) { + return; + } + } else if (length > 24) { + if (writeLatin1String25To31(stringBytes, length)) { + return; + } + } else if (length > 16) { + if (writeLatin1String17To24(stringBytes, length)) { + return; + } + } else if (length < 8) { + if (writeLatin1String0To7(stringBytes, length)) { + return; + } + } else { + byte[] bytes = buffer; + int pos = start; + bytes[pos++] = (byte) '"'; + latin1: + { + long word = LittleEndian.getInt64(stringBytes, 0); + if (!isJsonAsciiWord(word)) { + break latin1; + } + LittleEndian.putInt64(bytes, pos, word); + pos += Long.BYTES; + int index = Long.BYTES; + if (index + Long.BYTES <= length) { + long tail = LittleEndian.getInt64(stringBytes, index); + if (!isJsonAsciiWord(tail)) { + break latin1; + } + LittleEndian.putInt64(bytes, pos, tail); + pos += Long.BYTES; + index += Long.BYTES; + } + if (index + Integer.BYTES <= length) { + int tail = LittleEndian.getInt32(stringBytes, index); + if (!isJsonAsciiInt(tail)) { + break latin1; + } + LittleEndian.putInt32(bytes, pos, tail); + pos += Integer.BYTES; + index += Integer.BYTES; + } + if (index + Short.BYTES <= length) { + int tail = (stringBytes[index] & 0xFF) | ((stringBytes[index + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(tail)) { + break latin1; + } + bytes[pos] = (byte) tail; + bytes[pos + 1] = (byte) (tail >>> 8); + pos += Short.BYTES; + index += Short.BYTES; + } + if (index < length) { + byte tail = stringBytes[index]; + if (!isJsonAsciiByte(tail)) { + break latin1; + } + bytes[pos++] = tail; + } + bytes[pos++] = (byte) '"'; + position = pos; + return; + } } + position = start; } else { - if (writeUtf16String(bytes)) { + if (writeUtf16String(stringBytes)) { return; } } @@ -345,9 +429,37 @@ public void writeOffsetDateTime(OffsetDateTime value) { bytes[pos++] = (byte) '"'; pos = writeLocalDateBytes(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) 'T'; - pos = - writeTime( - bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); + // Keep the complete UTC common path in its representation owner so generated callers see a + // natural C2 boundary that does not depend on compilation order. + pos = writeTwoDigits(bytes, pos, value.getHour()); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, value.getMinute()); + int second = value.getSecond(); + int nano = value.getNano(); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + if (nano % 1_000_000 == 0) { + pos = writePadded3(bytes, pos, nano / 1_000_000); + } else if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + pos = writePadded3(bytes, pos, low); + } else { + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + pos = writePadded4(bytes, pos, low); + } + } + } bytes[pos++] = (byte) 'Z'; bytes[pos++] = (byte) '"'; position = pos; @@ -540,16 +652,18 @@ public void writeIntField(byte[] namePrefix, byte[] commaNamePrefix, int index, } public void writeIntField( - long namePrefix, - long commaPrefix, + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, int namePrefixLength, int commaPrefixLength, int index, int value) { if (index == 0) { - writeIntField(namePrefix, 0L, namePrefixLength, value); + writeIntField(namePrefix0, namePrefix1, namePrefixLength, value); } else { - writeIntField(commaPrefix, 0L, commaPrefixLength, value); + writeIntField(commaPrefix0, commaPrefix1, commaPrefixLength, value); } } @@ -590,13 +704,29 @@ public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, public void writeLongField(byte[] prefix, long value) { ensure(prefix.length + 20); writeRawNoEnsure(prefix); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); } public void writeLongField(long prefix0, long prefix1, int prefixLength, long value) { ensurePackedPrefix(prefixLength, 20); writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); + } + + public void writeLongField( + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, + int namePrefixLength, + int commaPrefixLength, + int index, + long value) { + if (index == 0) { + writeLongField(namePrefix0, namePrefix1, namePrefixLength, value); + } else { + writeLongField(commaPrefix0, commaPrefix1, commaPrefixLength, value); + } } public void writeObjectStartWithLongField(byte[] namePrefix, long value) { @@ -604,7 +734,7 @@ public void writeObjectStartWithLongField(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; writeRawNoEnsure(namePrefix); - writeLongNoEnsure(value); + writeLongFieldNoEnsure(value); } public void writeObjectStartWithLongField( @@ -613,13 +743,7 @@ public void writeObjectStartWithLongField( ensurePackedPrefix(prefixLength, 21); buffer[position++] = (byte) '{'; writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeLongNoEnsure(value); - } - - public void writeObjectStartWithStringField( - long prefix0, long prefix1, int prefixLength, String value) { - enterDepth(); - writeStringField(prefix0, prefix1, prefixLength, value); + writeLongFieldNoEnsure(value); } public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { @@ -628,75 +752,29 @@ public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int inde } public void writeStringField( - long namePrefix, - long commaPrefix, + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, int namePrefixLength, int commaPrefixLength, int index, String value) { if (index == 0) { - writeStringField(namePrefix, 0L, namePrefixLength, value); + writeStringField(namePrefix0, namePrefix1, namePrefixLength, value); } else { - writeStringField(commaPrefix, 0L, commaPrefixLength, value); + writeStringField(commaPrefix0, commaPrefix1, commaPrefixLength, value); } } public void writeStringField(byte[] prefix, String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensure(prefix.length + bytes.length + 2); - writeRawNoEnsure(prefix); - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensure(prefix.length + (bytes.length >> 1) * 3 + 2); - writeRawNoEnsure(prefix); - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } - } - writeStringFieldChars(prefix, value); + writeRaw(prefix); + writeString(value); } public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensurePackedPrefix(prefixLength, bytes.length + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } - } - writeStringFieldChars(prefix0, prefix1, prefixLength, value); - } - - private void writeStringFieldChars(byte[] prefix, String value) { - ensure(prefix.length + value.length() * 3 + 2); - writeRawNoEnsure(prefix); - writeStringNoEnsure(value); - } - - private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { - ensurePackedPrefix(prefixLength, value.length() * 3 + 2); - writePackedRawNoEnsure(prefix0, prefix1, prefixLength); - writeStringNoEnsure(value); + writeRawValue(prefix0, prefix1, prefixLength); + writeString(value); } public void writeStringCollection(Collection values) { @@ -737,6 +815,24 @@ public void writeStringArray(String[] values) { writeArrayEnd(); } + /** + * Writes a long array as one naturally independent generated-caller boundary. + * + *

The first element, even-length element, and two pair-loop lanes intentionally contain the + * same signed-Long to Int/full-width dispatch. The repeated real work keeps this method above + * HotSpot JDK 25's 325-byte hot-inline limit, so a generated object group retains only the array + * call and the array loop forms its own level-4 nmethod. A small shared element helper is not an + * equivalent cleanup: it can be inlined first and then pull the complete array closure into the + * generated caller according to compilation timing. + * + *

The duplication also preserves the pair-unrolled control flow, one capacity check per + * unrolled chunk, and direct {@code buffer}/{@code position} state. It is valid for Long encoding + * to repeat the Int-range fast path when that is faster than a generic numeric helper. Do not + * deduplicate these lanes unless generated-source, {@code javap}, PrintInlining, LogCompilation, + * nmethod, allocation, and intrinsic/aggregate benchmarks prove the same stable boundary and no + * regression. This is data-shape-independent code; never replace it with fixed array-length + * assumptions. + */ public void writeLongArray(long[] values) { enterDepth(); ensure(2); @@ -744,20 +840,83 @@ public void writeLongArray(long[] values) { int length = values.length; if (length != 0) { ensure(22); - writeLongNoEnsure(values[0]); + { + long value = values[0]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } + int i = 1; if ((length & 1) == 0) { ensure(22); buffer[position++] = ','; - writeLongNoEnsure(values[i]); + { + long value = values[i]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } i++; } + for (; i < length; i += 2) { ensure(44); buffer[position++] = ','; - writeLongNoEnsure(values[i]); + { + long value = values[i]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } + buffer[position++] = ','; - writeLongNoEnsure(values[i + 1]); + { + long value = values[i + 1]; + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + } else { + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + } else { + position = writePositiveLong(buffer, position, value); + } + } + } } } buffer[position++] = ']'; @@ -773,30 +932,10 @@ private void writeStringElementWithComma(int comma, String value) { writeNullStringElement(comma); return; } - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - int start = position; - if (bytes.length == value.length()) { - ensure(comma + bytes.length + 2); - if (comma != 0) { - buffer[position++] = ','; - } - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - position = start; - } else { - ensure(comma + (bytes.length >> 1) * 3 + 2); - if (comma != 0) { - buffer[position++] = ','; - } - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - position = start; - } + if (comma != 0) { + writeByteRaw((byte) ','); } - writeStringElementChars(comma, value); + writeString(value); } private void writeNullStringElement(int comma) { @@ -807,14 +946,6 @@ private void writeNullStringElement(int comma) { writeAsciiNoEnsure("null"); } - private void writeStringElementChars(int comma, String value) { - ensure(comma + value.length() * 3 + 2); - if (comma != 0) { - buffer[position++] = ','; - } - writeStringNoEnsure(value); - } - public void writeRawValue(byte[] value) { writeRaw(value); } @@ -860,6 +991,21 @@ public void writeRawValue(long prefix0, long prefix1, int prefixLength) { writePackedRawNoEnsure(prefix0, prefix1, prefixLength); } + public void writeRawValue( + long namePrefix0, + long namePrefix1, + long commaPrefix0, + long commaPrefix1, + int namePrefixLength, + int commaPrefixLength, + int index) { + if (index == 0) { + writeRawValue(namePrefix0, namePrefix1, namePrefixLength); + } else { + writeRawValue(commaPrefix0, commaPrefix1, commaPrefixLength); + } + } + /** Writes a byte array as a quoted Base64 JSON string without an intermediate String. */ public void writeBase64(byte[] value) { int encodedLength = base64Length(value.length); @@ -979,24 +1125,6 @@ public void writeComma(int index) { } } - private boolean writeLatin1String(byte[] value) { - int length = value.length; - ensure(length + 2); - return writeLatin1StringNoEnsure(value, length); - } - - private boolean writeLatin1StringNoEnsure(byte[] value) { - int length = value.length; - return writeLatin1StringNoEnsure(value, length); - } - - private boolean writeLatin1StringNoEnsure(byte[] value, int length) { - if (length < 32) { - return writeShortLatin1StringNoEnsure(value, length); - } - return writeLongLatin1StringNoEnsure(value, length); - } - private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; @@ -1053,73 +1181,6 @@ private static boolean isJsonAsciiBytes(byte[] value, int length) { return true; } - private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { - if (length > 24) { - return writeLatin1String25To31(value, length); - } - if (length > 16) { - return writeLatin1String17To24(value, length); - } - byte[] bytes = buffer; - int pos = position; - bytes[pos++] = (byte) '"'; - // Short compact strings dominate generated JSON writers. Keep the 8-16 byte path exact here; - // longer short-string bands stay in helpers so this common path remains small. - // Position is published only after complete validation, so every false return leaves the - // writer cursor unchanged even though scratch bytes may already have been overwritten. - if (length >= 8) { - long word = LittleEndian.getInt64(value, 0); - if (!isJsonAsciiWord(word)) { - return false; - } - LittleEndian.putInt64(bytes, pos, word); - pos += 8; - int index = Long.BYTES; - if (index + Long.BYTES <= length) { - long tail = LittleEndian.getInt64(value, index); - if (!isJsonAsciiWord(tail)) { - return false; - } - LittleEndian.putInt64(bytes, pos, tail); - pos += Long.BYTES; - index += Long.BYTES; - } - if (index + Integer.BYTES <= length) { - int tail = LittleEndian.getInt32(value, index); - if (!isJsonAsciiInt(tail)) { - return false; - } - LittleEndian.putInt32(bytes, pos, tail); - pos += Integer.BYTES; - index += Integer.BYTES; - } - if (index + Short.BYTES <= length) { - int tail = (value[index] & 0xFF) | ((value[index + 1] & 0xFF) << 8); - if (!isJsonAsciiShort(tail)) { - return false; - } - bytes[pos] = (byte) tail; - bytes[pos + 1] = (byte) (tail >>> 8); - pos += Short.BYTES; - index += Short.BYTES; - } - if (index < length) { - byte tail = value[index]; - if (!isJsonAsciiByte(tail)) { - return false; - } - bytes[pos++] = tail; - } - } else { - // Keep the sub-8 tail outside this method so the common word-sized short-string path stays - // small enough to inline into generated object writers. - return writeLatin1String0To7(value, length); - } - bytes[pos++] = (byte) '"'; - position = pos; - return true; - } - private boolean writeLatin1String0To7(byte[] value, int length) { byte[] bytes = buffer; int pos = position; @@ -1430,59 +1491,6 @@ private void writeDurationFraction(int value) { } } - private void writeStringNoEnsure(String value) { - if (STRING_BYTES_BACKED) { - byte[] bytes = StringSerializer.getStringBytes(value); - if (bytes.length == value.length()) { - if (writeLatin1StringNoEnsure(bytes)) { - return; - } - } else { - if (writeUtf16StringNoEnsure(bytes)) { - return; - } - } - } - writeStringCharsNoEnsure(value); - } - - private void writeStringCharsNoEnsure(String value) { - int length = value.length(); - byte[] bytes = buffer; - int pos = position; - bytes[pos++] = (byte) '"'; - int i = 0; - while (i + 4 <= length) { - char c0 = value.charAt(i); - char c1 = value.charAt(i + 1); - char c2 = value.charAt(i + 2); - char c3 = value.charAt(i + 3); - if (isJsonAscii(c0) && isJsonAscii(c1) && isJsonAscii(c2) && isJsonAscii(c3)) { - bytes[pos] = (byte) c0; - bytes[pos + 1] = (byte) c1; - bytes[pos + 2] = (byte) c2; - bytes[pos + 3] = (byte) c3; - pos += 4; - i += 4; - } else { - break; - } - } - while (i < length) { - char ch = value.charAt(i); - if (isJsonAscii(ch)) { - bytes[pos++] = (byte) ch; - i++; - } else { - position = pos; - writeStringSlow(value, i, length); - return; - } - } - bytes[pos++] = (byte) '"'; - position = pos; - } - private void writeCodePoint(int codePoint) { ensure(4); buffer[position++] = (byte) (0xF0 | (codePoint >>> 18)); @@ -1630,7 +1638,7 @@ private void writeCompactBigDecimal(long unscaled, int scale) { return; } if (precision == 1) { - writePositiveLongNoEnsure(unscaled); + writeLongNoEnsure(unscaled); } else { long divisor = BigNumberDigits.LONG_POWERS_OF_TEN[precision - 1]; int first = (int) (unscaled / divisor); @@ -1884,6 +1892,11 @@ private void writeIntNoEnsure(int value) { writePositiveIntNoEnsure(value); } + // Keep general Long and generated-field Long entries separate even though their common code is + // intentionally repeated. They have different range profiles and callers; merging them into a + // generic scalar helper lets one profile dictate the other's inline shape. Reusing the + // independently compiled positive Int and full-width Long leaves is safe because those leaves + // own complete formatting work and pass buffer/cursor state explicitly. private void writeLongNoEnsure(long value) { if (value == Long.MIN_VALUE) { writeRawNoEnsure(MIN_LONG_BYTES); @@ -1893,34 +1906,110 @@ private void writeLongNoEnsure(long value) { buffer[position++] = (byte) '-'; value = -value; } - writePositiveLongNoEnsure(value); - } - - private void writePositiveIntNoEnsure(int value) { - position = writePositiveInt(buffer, position, value); + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + position = writePositiveLong(buffer, position, value); } - private void writePositiveLongNoEnsure(long value) { + // This duplicate entry is owned by generated object fields. Do not merge it with + // writeLongNoEnsure merely to remove source repetition: field and scalar callers have independent + // range/type profiles, while array element dispatch is deliberately owned by writeLongArray. + private void writeLongFieldNoEnsure(long value) { + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + return; + } + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } if (value <= Integer.MAX_VALUE) { writePositiveIntNoEnsure((int) value); return; } - byte[] bytes = buffer; - int pos = position; + position = writePositiveLong(buffer, position, value); + } + + private void writePositiveIntNoEnsure(int value) { + position = writePositiveInt(buffer, position, value); + } + + // The full-width formatter is one real-work owner shared by the deliberately separate scalar, + // field, and array entries above. Passing the byte array and cursor and returning the new cursor + // lets each caller keep buffer/position state live across the call and publish position once. + // Do not replace this with a writer callback, carrier object, or mutable-writer lookup. Whether + // this leaf inlines is measured independently; the guaranteed greater-than-325-BCI boundary for + // long[] is writeLongArray, so do not copy this formatter into generated callers merely to make + // another method large. + private static int writePositiveLong(byte[] bytes, int pos, long value) { long high = value / EIGHT_DIGITS; int low = (int) (value - high * EIGHT_DIGITS); if (high <= Integer.MAX_VALUE) { - pos = writePositiveInt(bytes, pos, (int) high); + int highValue = (int) high; + if (highValue < 10000) { + if (highValue < 1000) { + int digits = DIGIT_TRIPLES[highValue]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + } else { + LittleEndian.putInt32(bytes, pos, DIGIT_QUADS[highValue]); + pos += 4; + } + } else { + int highHigh = divide10000(highValue); + int highLow = highValue - highHigh * 10000; + if (highHigh < 10000) { + if (highHigh >= 1000) { + LittleEndian.putInt64( + bytes, + pos, + (DIGIT_QUADS[highHigh] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[highLow] << 32)); + pos += 8; + } else { + int digits = DIGIT_TRIPLES[highHigh]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + LittleEndian.putInt32(bytes, pos, DIGIT_QUADS[highLow]); + pos += 4; + } + } else { + int top = divide10000(highHigh); + int middle = highHigh - top * 10000; + int digits = DIGIT_TRIPLES[top]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + LittleEndian.putInt64( + bytes, + pos, + (DIGIT_QUADS[middle] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[highLow] << 32)); + pos += 8; + } + } } else { long top = high / EIGHT_DIGITS; int middle = (int) (high - top * EIGHT_DIGITS); - // A positive long has at most 19 decimal digits, so removing two eight-digit chunks leaves a - // top chunk in [1, 922]. Bypass the general int branch tree for this proven three-digit - // bound. - pos = writeIntUpTo3(bytes, pos, (int) top); - pos = writePadded8Digits(bytes, pos, middle); + int digits = DIGIT_TRIPLES[(int) top]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + pos += 3 - skip; + int middleHigh = divide10000(middle); + int middleLow = middle - middleHigh * 10000; + LittleEndian.putInt64( + bytes, + pos, + (DIGIT_QUADS[middleHigh] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[middleLow] << 32)); + pos += 8; } - position = writePadded8Digits(bytes, pos, low); + int lowHigh = divide10000(low); + int lowLow = low - lowHigh * 10000; + LittleEndian.putInt64( + bytes, pos, (DIGIT_QUADS[lowHigh] & 0xFFFFFFFFL) | ((long) DIGIT_QUADS[lowLow] << 32)); + return pos + 8; } private static int writePositiveInt(byte[] bytes, int pos, int value) { @@ -1980,41 +2069,6 @@ private static int writeLocalDateBytes(byte[] bytes, int pos, int year, int mont return writeTwoDigits(bytes, pos, day); } - private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { - pos = writeTwoDigits(bytes, pos, hour); - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, minute); - if (second != 0 || nano != 0) { - bytes[pos++] = (byte) ':'; - pos = writeTwoDigits(bytes, pos, second); - if (nano != 0) { - bytes[pos++] = (byte) '.'; - pos = writeNano(bytes, pos, nano); - } - } - return pos; - } - - private static int writeNano(byte[] bytes, int pos, int nano) { - if (nano % 1_000_000 == 0) { - return writePadded3(bytes, pos, nano / 1_000_000); - } - if (nano % 1000 == 0) { - int micros = nano / 1000; - int high = micros / 1000; - int low = micros - high * 1000; - pos = writePadded3(bytes, pos, high); - return writePadded3(bytes, pos, low); - } - int first = nano / 100000000; - int rem = nano - first * 100000000; - int middle = rem / 10000; - int low = rem - middle * 10000; - bytes[pos++] = (byte) ('0' + first); - pos = writePadded4(bytes, pos, middle); - return writePadded4(bytes, pos, low); - } - private static int writePadded3(byte[] bytes, int pos, int value) { int high = value / 100; int rem = value - high * 100; diff --git a/java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java b/java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java new file mode 100644 index 0000000000..5a75dd0779 --- /dev/null +++ b/java/fory-json/src/main/java25/org/apache/fory/json/reader/DecimalMath.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** JDK 25 exact integer arithmetic used by decimal readers. */ +final class DecimalMath { + private DecimalMath() {} + + static long unsignedMultiplyHigh(long x, long y) { + return Math.unsignedMultiplyHigh(x, y); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index c3e8b9d731..a4cac821e6 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -25,6 +25,7 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; @@ -34,14 +35,19 @@ import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -58,6 +64,7 @@ import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.Latin1ReaderCodec; import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.StringWriterCodec; import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; @@ -75,7 +82,6 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.reflect.TypeRef; -import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; public class JsonAsyncCompilationTest { @@ -87,48 +93,34 @@ public void defaultBuilderEnablesAsync() throws Exception { } @Test - public void capabilitiesCompileLazily() throws Exception { + public void capabilitiesRegisterAfterResolution() throws Exception { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); JsonTypeResolver resolver = primaryTypeResolver(json); ObjectCodec owner = resolver.getObjectCodec(AsyncChild.class); JsonTypeInfo info = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); - assertCapabilities(info, owner); + assertNotSame(info.stringWriter(), owner); + assertNotSame(info.utf8Writer(), owner); + assertNotSame(info.latin1Reader(), owner); + assertNotSame(info.utf16Reader(), owner); + assertNotSame(info.utf8Reader(), owner); AsyncChild value = child("root", 1); assertEquals(json.toJson(value), "{\"id\":1,\"name\":\"root\"}"); - assertNotSame(info.stringWriter(), owner); - assertSame(info.utf8Writer(), owner); - assertSame(info.latin1Reader(), owner); - assertSame(info.utf16Reader(), owner); - assertSame(info.utf8Reader(), owner); - assertEquals( new String(json.toJsonBytes(value), StandardCharsets.UTF_8), "{\"id\":1,\"name\":\"root\"}"); - assertNotSame(info.utf8Writer(), owner); assertEquals(json.fromJson("{\"id\":2,\"name\":\"latin\"}", AsyncChild.class).id, 2); - if (StringSerializer.isBytesBackedString()) { - assertNotSame(info.latin1Reader(), owner); - assertSame(info.utf16Reader(), owner); - } else { - assertSame(info.latin1Reader(), owner); - assertNotSame(info.utf16Reader(), owner); - resolver.latin1Reader(owner); - assertNotSame(info.latin1Reader(), owner); - } assertEquals(json.fromJson("{\"id\":3,\"name\":\"\u4f60\"}", AsyncChild.class).id, 3); - assertNotSame(info.utf16Reader(), owner); assertEquals( json.fromJson( "{\"id\":4,\"name\":\"utf8\"}".getBytes(StandardCharsets.UTF_8), AsyncChild.class) .id, 4); - assertNotSame(info.utf8Reader(), owner); assertSame(resolver.getObjectCodec(AsyncChild.class), owner); } @Test - public void inlineAnyReadersInstall() throws Exception { + public void closedSubtypeBranchesInstall() throws Exception { ControlledJson controlled = controlledJson(); ForyJson json = controlled.json; AsyncInlineChild initial = @@ -137,12 +129,17 @@ public void inlineAnyReadersInstall() throws Exception { controlled.executor.runAll(); JsonTypeResolver resolver = primaryTypeResolver(json); - ClosedSubtypeCodec codec = - (ClosedSubtypeCodec) - resolver.getTypeInfo(AsyncInlineShape.class, AsyncInlineShape.class).latin1Reader(); - assertNotNull(((Object[]) field(codec, "inlineLatin1Readers"))[0]); - assertNotNull(((Object[]) field(codec, "inlineUtf16Readers"))[0]); - assertNotNull(((Object[]) field(codec, "inlineUtf8Readers"))[0]); + JsonTypeInfo shapeInfo = resolver.getTypeInfo(AsyncInlineShape.class, AsyncInlineShape.class); + assertTrue(shapeInfo.latin1Reader() instanceof ClosedSubtypeCodec); + ClosedSubtypeCodec subtype = (ClosedSubtypeCodec) shapeInfo.latin1Reader(); + JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncInlineChild.class, AsyncInlineChild.class); + ObjectCodec childOwner = resolver.getObjectCodec(AsyncInlineChild.class); + assertNotSame(childInfo.latin1Reader(), childOwner); + assertNotSame(childInfo.utf16Reader(), childOwner); + assertNotSame(childInfo.utf8Reader(), childOwner); + assertInlineReader(subtype.inlineLatin1Readers(), childInfo.latin1Reader()); + assertInlineReader(subtype.inlineUtf16Readers(), childInfo.utf16Reader()); + assertInlineReader(subtype.inlineUtf8Readers(), childInfo.utf8Reader()); AsyncInlineChild latin1 = (AsyncInlineChild) json.fromJson("{\"x\":2,\"kind\":\"child\"}", AsyncInlineShape.class); @@ -176,17 +173,6 @@ public void creatorCapabilitiesInstall() throws Exception { 4); JsonTypeResolver resolver = primaryTypeResolver(json); ObjectCodec owner = resolver.getObjectCodec(AsyncCreator.class); - if (!StringSerializer.isBytesBackedString()) { - // Java 8 strings are char-backed, so public String reads select the UTF-16 reader and cannot - // request Latin-1 compilation. Explicitly request that cold capability to keep this test's - // coverage of every asynchronously installed codec independent of the JDK string layout. - resolver.lockJIT(); - try { - assertSame(resolver.latin1Reader(owner), owner); - } finally { - resolver.unlockJIT(); - } - } controlled.executor.runAll(); JsonTypeInfo info = resolver.getTypeInfo(AsyncCreator.class, AsyncCreator.class); assertNotSame(info.stringWriter(), owner); @@ -221,7 +207,7 @@ public void asyncInstancesAreResolverLocal() throws Exception { } finally { second.unlockJIT(); } - assertEquals(controlled.executor.pendingTasks(), 2); + assertEquals(controlled.executor.pendingTasks(), 5); controlled.executor.runAll(); StringWriterCodec firstWriter = stringWriter(first, firstOwner); @@ -301,7 +287,7 @@ public void asyncCompletionPublishesAllPaths() throws Exception { } @Test - public void duplicateCallbacksPublish() throws Exception { + public void accessorsDoNotReregisterCapabilities() throws Exception { ControlledJson controlled = controlledJson(); JsonTypeResolver resolver = new JsonTypeResolver(controlled.registry); resolver.lockJIT(); @@ -315,8 +301,13 @@ public void duplicateCallbacksPublish() throws Exception { resolver.unlockJIT(); } - assertEquals(controlled.executor.submittedTasks(), 2); - assertEquals(controlled.executor.pendingTasks(), 2); + assertEquals(controlled.executor.submittedTasks(), 5); + assertEquals(controlled.executor.pendingTasks(), 5); + for (int i = 0; i < 100; i++) { + assertSame(stringWriter(resolver, owner), owner); + } + assertEquals(controlled.executor.submittedTasks(), 5); + assertEquals(controlled.executor.pendingTasks(), 5); controlled.executor.runAll(); assertNotSame(stringWriter(resolver, owner), owner); } @@ -332,22 +323,23 @@ public void rolledBackReaderTasksStayIsolated() throws Exception { expectThrows( ForyJsonException.class, () -> resolver.getTypeInfo(BrokenShape.class, BrokenShape.class)); - assertEquals(controlled.executor.pendingTasks(), 3); + assertEquals(controlled.executor.pendingTasks(), 0); replacementOwner = resolver.getObjectCodec(RollbackCircle.class); replacement = resolver.getTypeInfo(RollbackCircle.class, RollbackCircle.class); assertSame(replacement.latin1Reader(), replacementOwner); assertSame(replacement.utf16Reader(), replacementOwner); assertSame(replacement.utf8Reader(), replacementOwner); + assertEquals(controlled.executor.pendingTasks(), 5); } finally { resolver.unlockJIT(); } - // The failed subtype transaction queued parent-local Any readers against its provisional - // slots. Completing those tasks must not mutate the replacement generation for the same class. + // A failed metadata transaction cannot register a capability graph. The later independent + // binding registers exactly its own five representations and cannot be mutated by stale work. controlled.executor.runAll(); - assertSame(replacement.latin1Reader(), replacementOwner); - assertSame(replacement.utf16Reader(), replacementOwner); - assertSame(replacement.utf8Reader(), replacementOwner); + assertNotSame(replacement.latin1Reader(), replacementOwner); + assertNotSame(replacement.utf16Reader(), replacementOwner); + assertNotSame(replacement.utf8Reader(), replacementOwner); } @Test @@ -371,33 +363,34 @@ public void rollbackClearsCanonicalOwners() throws Exception { @Test public void asyncFailureKeepsInterpretedResult() { - ControlledExecutor executor = new ControlledExecutor(); - JsonJITContext context = new JsonJITContext(true, executor); + JsonJITContext context = new JsonJITContext(true); + CompletableFuture future = new CompletableFuture<>(); AtomicReference failure = new AtomicReference<>(); - int result = - context.registerJITCallback( - () -> 1, - () -> { - throw new IllegalStateException("compile failure"); - }, - new JsonJITContext.JITCallback() { - @Override - public void onSuccess(Integer generated) { - fail("Unexpected generated result"); - } - - @Override - public void onFailure(Throwable throwable) { - failure.set(throwable); - } - - @Override - public Object id() { - return "failure"; - } - }); - assertEquals(result, 1); - executor.runNext(); + JsonJITContext.JITCallback callback = + new JsonJITContext.JITCallback() { + @Override + public void onSuccess(Integer generated) { + fail("Unexpected generated result"); + } + + @Override + public void onFailure(Throwable throwable) { + failure.set(throwable); + } + + @Override + public Object id() { + return "failure"; + } + }; + context.registerJITFuture(() -> future, callback); + context.registerJITFuture( + () -> { + fail("Duplicate active request"); + return CompletableFuture.completedFuture(2); + }, + callback); + future.completeExceptionally(new IllegalStateException("compile failure")); assertTrue(failure.get() instanceof IllegalStateException); } @@ -408,17 +401,6 @@ public void rootAndCompletionUseLocalLock() throws Exception { CodecRegistry codecs = new CodecRegistry(); codecs.register(BlockingValue.class, new BlockingCodec(rootEntered, releaseRoot)); ControlledJson controlled = controlledJson(codecs); - JsonTypeResolver compilerResolver = new JsonTypeResolver(controlled.registry); - compilerResolver.lockJIT(); - try { - ObjectCodec owner = compilerResolver.getObjectCodec(AsyncChild.class); - compilerResolver.getTypeInfo(AsyncChild.class, AsyncChild.class); - assertSame(compilerResolver.stringWriter(owner), owner); - } finally { - compilerResolver.unlockJIT(); - } - controlled.executor.runNext(); - JsonTypeResolver resolver = primaryTypeResolver(controlled.json); resolver.lockJIT(); ObjectCodec owner; @@ -462,6 +444,7 @@ public void rootAndCompletionUseLocalLock() throws Exception { installer.start(); await(installStarted); assertSame(info.stringWriter(), owner); + assertFalse(installFinished.await(100, TimeUnit.MILLISECONDS)); releaseRoot.countDown(); root.join(); await(installFinished); @@ -575,32 +558,24 @@ public void pooledStatesRemainConcurrent() throws Exception { second.start(); await(secondFinished); assertFailure(secondFailure.get()); - assertEquals(controlled.executor.pendingTasks(), 1); + assertEquals(controlled.executor.pendingTasks(), 5); releaseRoot.countDown(); first.join(); assertFailure(firstFailure.get()); - controlled.executor.runNext(); + controlled.executor.runAll(); } @Test public void capabilityFailureIsIndependent() throws Exception { ControlledJson controlled = controlledJson(); - JsonTypeResolver resolver = primaryTypeResolver(controlled.json); - resolver.lockJIT(); - ObjectCodec owner; - JsonTypeInfo info; - try { - owner = resolver.getObjectCodec(AsyncChild.class); - info = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); - } finally { - resolver.unlockJIT(); - } controlled.executor.rejectNext(); - expectThrows(RejectedExecutionException.class, () -> controlled.json.toJson(child("x", 1))); + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + assertEquals(controlled.json.toJson(child("x", 1)), "{\"id\":1,\"name\":\"x\"}"); + ObjectCodec owner = resolver.getObjectCodec(AsyncChild.class); + JsonTypeInfo info = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); assertSame(info.stringWriter(), owner); - - controlled.json.toJsonBytes(child("x", 1)); - controlled.executor.runNext(); + assertEquals(controlled.executor.pendingTasks(), 4); + controlled.executor.runAll(); assertNotSame(info.utf8Writer(), owner); assertSame(info.stringWriter(), owner); } @@ -624,7 +599,7 @@ public void semanticBindingsRemainOwners() throws Exception { try { parameterized = resolver.getTypeInfo(declaredType.getType(), GenericAsyncBox.class); parameterizedReader = parameterized.utf8Reader(); - assertFalse(parameterized.usesDefaultObjectCodec()); + assertNull(resolver.canonicalObjectCodec(parameterized)); } finally { resolver.unlockJIT(); } @@ -633,6 +608,14 @@ public void semanticBindingsRemainOwners() throws Exception { "{\"value\":\"raw\"}".getBytes(StandardCharsets.UTF_8), GenericAsyncBox.class); controlled.executor.runNext(); assertSame(parameterized.utf8Reader(), parameterizedReader); + resolver.lockJIT(); + try { + JsonTypeInfo raw = resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class); + assertSame( + resolver.canonicalObjectCodec(raw), resolver.getObjectCodec(GenericAsyncBox.class)); + } finally { + resolver.unlockJIT(); + } JsonValueCodec codec = nullCodec(); CodecRegistry codecs = new CodecRegistry(); @@ -646,9 +629,375 @@ public void semanticBindingsRemainOwners() throws Exception { JsonTypeInfo customInfo = primaryTypeResolver(custom.json).getTypeInfo(AsyncChild.class, AsyncChild.class); assertCapabilities(customInfo, codec); + assertNull(primaryTypeResolver(custom.json).canonicalObjectCodec(customInfo)); assertEquals(custom.executor.submittedTasks(), 0); } + @Test + public void sourceShapeIgnoresPublicationOrder() { + ForyJson parentFirstJson = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver parentFirstResolver = primaryTypeResolver(parentFirstJson); + ObjectCodec parentFirstOwner = + parentFirstResolver.getObjectCodec(AsyncParent.class); + parentFirstResolver.getTypeInfo(AsyncParent.class, AsyncParent.class); + Object parentFirst = parentFirstResolver.utf8Writer(parentFirstOwner); + + ForyJson childFirstJson = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver childFirstResolver = primaryTypeResolver(childFirstJson); + ObjectCodec childFirstOwner = childFirstResolver.getObjectCodec(AsyncChild.class); + childFirstResolver.getTypeInfo(AsyncChild.class, AsyncChild.class); + childFirstResolver.utf8Writer(childFirstOwner); + ObjectCodec childFirstParent = + childFirstResolver.getObjectCodec(AsyncParent.class); + childFirstResolver.getTypeInfo(AsyncParent.class, AsyncParent.class); + Object childFirst = childFirstResolver.utf8Writer(childFirstParent); + + assertEquals(declaredFieldTypes(parentFirst), declaredFieldTypes(childFirst)); + assertTrue(declaredFieldTypes(parentFirst).contains(Utf8WriterCodec.class.getName())); + } + + @Test + public void utf8GraphPublishesAtomically() throws Exception { + ControlledJson controlled = controlledJson(); + String input = + "{\"children\":[{\"id\":1,\"name\":\"a\"},{\"id\":2,\"name\":\"b\"}," + + "{\"id\":3,\"name\":\"c\"},{\"id\":4,\"name\":\"d\"}," + + "{\"id\":5,\"name\":\"e\"},{\"id\":6,\"name\":\"f\"}," + + "{\"id\":7,\"name\":\"g\"},{\"id\":8,\"name\":\"h\"}," + + "{\"id\":9,\"name\":\"i\"}],\"friends\":[{\"id\":10}]}"; + AsyncCollections initial = + controlled.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncCollections.class); + assertEquals(initial.children.size(), 9); + assertEquals(initial.friends.get(0).id, 10); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec rootOwner = resolver.getObjectCodec(AsyncCollections.class); + ObjectCodec childOwner = resolver.getObjectCodec(AsyncChild.class); + ObjectCodec friendOwner = resolver.getObjectCodec(AsyncFriend.class); + JsonTypeInfo rootInfo = resolver.getTypeInfo(AsyncCollections.class, AsyncCollections.class); + JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); + JsonTypeInfo friendInfo = resolver.getTypeInfo(AsyncFriend.class, AsyncFriend.class); + JsonFieldInfo[] fields = rootOwner.readFields(); + JsonTypeInfo childrenInfo = fields[0].readTypeInfo(); + JsonTypeInfo friendsInfo = fields[1].readTypeInfo(); + Object initialRootReader = rootInfo.utf8Reader(); + Object initialChildReader = childInfo.utf8Reader(); + Object initialFriendReader = friendInfo.utf8Reader(); + Object initialChildren = childrenInfo.utf8Reader(); + Object initialFriends = friendsInfo.utf8Reader(); + JsonFieldInfo[] writeFields = rootOwner.writeFields(); + JsonTypeInfo writtenChildrenInfo = writeFields[0].writeTypeInfo(); + JsonTypeInfo writtenFriendsInfo = writeFields[1].writeTypeInfo(); + Object initialRootWriter = rootInfo.utf8Writer(); + Object initialChildWriter = childInfo.utf8Writer(); + Object initialFriendWriter = friendInfo.utf8Writer(); + Object initialChildrenWriter = writtenChildrenInfo.utf8Writer(); + Object initialFriendsWriter = writtenFriendsInfo.utf8Writer(); + assertEquals(new String(controlled.json.toJsonBytes(initial), StandardCharsets.UTF_8), input); + + int pendingTasks = controlled.executor.pendingTasks(); + assertEquals(pendingTasks, 19); + for (int i = 0; i < pendingTasks; i++) { + controlled.executor.runNext(); + boolean initialReaderGraph = + rootInfo.utf8Reader() == initialRootReader + && childInfo.utf8Reader() == initialChildReader + && friendInfo.utf8Reader() == initialFriendReader + && childrenInfo.utf8Reader() == initialChildren + && friendsInfo.utf8Reader() == initialFriends; + boolean generatedReaderGraph = + rootInfo.utf8Reader() != initialRootReader + && childInfo.utf8Reader() != initialChildReader + && friendInfo.utf8Reader() != initialFriendReader + && childrenInfo.utf8Reader() != initialChildren + && friendsInfo.utf8Reader() != initialFriends; + assertTrue(initialReaderGraph || generatedReaderGraph); + + boolean initialWriterGraph = + rootInfo.utf8Writer() == initialRootWriter + && childInfo.utf8Writer() == initialChildWriter + && friendInfo.utf8Writer() == initialFriendWriter + && writtenChildrenInfo.utf8Writer() == initialChildrenWriter + && writtenFriendsInfo.utf8Writer() == initialFriendsWriter; + boolean generatedWriterGraph = + rootInfo.utf8Writer() != initialRootWriter + && childInfo.utf8Writer() != initialChildWriter + && friendInfo.utf8Writer() != initialFriendWriter + && writtenChildrenInfo.utf8Writer() != initialChildrenWriter + && writtenFriendsInfo.utf8Writer() != initialFriendsWriter; + assertTrue(initialWriterGraph || generatedWriterGraph); + } + + assertNotSame(rootInfo.utf8Reader(), rootOwner); + assertNotSame(childInfo.utf8Reader(), childOwner); + assertNotSame(friendInfo.utf8Reader(), friendOwner); + assertNotSame(childrenInfo.utf8Reader(), initialChildren); + assertNotSame(friendsInfo.utf8Reader(), initialFriends); + assertTrue(childrenInfo.utf8Reader().getClass() != friendsInfo.utf8Reader().getClass()); + assertFinalField(childrenInfo.utf8Reader(), "elementReader", childInfo.utf8Reader()); + assertFinalField(friendsInfo.utf8Reader(), "elementReader", friendInfo.utf8Reader()); + assertFinalCollectionFields( + rootInfo.utf8Reader(), + Utf8ReaderCodec.class, + childrenInfo.utf8Reader(), + friendsInfo.utf8Reader()); + + assertNotSame(rootInfo.utf8Writer(), initialRootWriter); + assertNotSame(childInfo.utf8Writer(), initialChildWriter); + assertNotSame(friendInfo.utf8Writer(), initialFriendWriter); + assertNotSame(writtenChildrenInfo.utf8Writer(), initialChildrenWriter); + assertNotSame(writtenFriendsInfo.utf8Writer(), initialFriendsWriter); + assertTrue( + writtenChildrenInfo.utf8Writer().getClass() != writtenFriendsInfo.utf8Writer().getClass()); + assertFinalField(writtenChildrenInfo.utf8Writer(), "elementWriter", childInfo.utf8Writer()); + assertFinalField(writtenFriendsInfo.utf8Writer(), "elementWriter", friendInfo.utf8Writer()); + assertFinalField(writtenChildrenInfo.utf8Writer(), "fallback", initialChildrenWriter); + assertFinalField(writtenFriendsInfo.utf8Writer(), "fallback", initialFriendsWriter); + assertFinalCollectionFields( + rootInfo.utf8Writer(), + Utf8WriterCodec.class, + writtenChildrenInfo.utf8Writer(), + writtenFriendsInfo.utf8Writer()); + assertEquals(writeUtf8(writtenChildrenInfo.utf8Writer(), null), "null"); + assertEquals(writeUtf8(writtenFriendsInfo.utf8Writer(), new ArrayList<>()), "[]"); + + AsyncCollections generated = + controlled.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncCollections.class); + assertEquals(generated.children.size(), 9); + assertEquals(generated.children.get(8).id, 9); + assertEquals(generated.friends.get(0).id, 10); + assertEquals(new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), input); + + AsyncCollections fallback = new AsyncCollections(); + fallback.children = new LinkedList<>(generated.children); + fallback.friends = new LinkedList<>(generated.friends); + assertEquals(new String(controlled.json.toJsonBytes(fallback), StandardCharsets.UTF_8), input); + + AsyncCollections empty = + controlled.json.fromJson( + "{\"children\":[],\"friends\":null}".getBytes(StandardCharsets.UTF_8), + AsyncCollections.class); + assertTrue(empty.children.isEmpty()); + assertNull(empty.friends); + assertEquals( + new String(controlled.json.toJsonBytes(empty), StandardCharsets.UTF_8), + "{\"children\":[]}"); + } + + @Test + public void utf8ParentsCaptureFinalChild() throws Exception { + ControlledJson controlled = controlledJson(); + byte[] creatorInput = + "{\"child\":{\"id\":1,\"name\":\"creator\"}}".getBytes(StandardCharsets.UTF_8); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec creatorOwner = + resolver.getObjectCodec(AsyncCreatorParent.class); + ObjectCodec graphOwner = resolver.getObjectCodec(AsyncGraphParent.class); + ObjectCodec childOwner = resolver.getObjectCodec(AsyncChild.class); + JsonTypeInfo creatorInfo = + resolver.getTypeInfo(AsyncCreatorParent.class, AsyncCreatorParent.class); + JsonTypeInfo graphInfo = resolver.getTypeInfo(AsyncGraphParent.class, AsyncGraphParent.class); + JsonTypeInfo childInfo = resolver.getTypeInfo(AsyncChild.class, AsyncChild.class); + resolver.lockJIT(); + try { + assertSame(resolver.utf8Reader(creatorOwner), creatorOwner); + assertSame(resolver.utf8Reader(graphOwner), graphOwner); + } finally { + resolver.unlockJIT(); + } + + assertEquals(controlled.executor.pendingTasks(), 15); + controlled.executor.runAll(); + assertNotSame(creatorInfo.utf8Reader(), creatorOwner); + assertNotSame(graphInfo.utf8Reader(), graphOwner); + assertNotSame(childInfo.utf8Reader(), childOwner); + assertCapabilityFields( + creatorInfo.utf8Reader(), Utf8ReaderCodec.class, childInfo.utf8Reader(), 1); + assertCapabilityFields( + graphInfo.utf8Reader(), Utf8ReaderCodec.class, childInfo.utf8Reader(), 1); + assertEquals( + controlled.json.fromJson(creatorInput, AsyncCreatorParent.class).child.name, "creator"); + } + + @Test + public void utf8ScalarCollectionCapability() throws Exception { + ControlledJson controlled = controlledJson(); + String[] tokens = { + "null", + "\"\"", + "\"short\"", + "\"abcdefghijklmnopqrstuvwxyz0123456789\"", + "\"quote\\\"\"", + "\"slash\\\\\"", + "\"line\\n\"", + "\"你好\"", + "\"eight\"", + "\"nine\"", + "\"ten\"", + "null", + "\"tail\"" + }; + List expected = + Arrays.asList( + null, + "", + "short", + "abcdefghijklmnopqrstuvwxyz0123456789", + "quote\"", + "slash\\", + "line\n", + "你好", + "eight", + "nine", + "ten", + null, + "tail"); + String input = stringCollectionInput(tokens, tokens.length); + AsyncStringCollections initial = + controlled.json.fromJson( + input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); + assertEquals(initial.values, expected); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec rootOwner = + resolver.getObjectCodec(AsyncStringCollections.class); + JsonTypeInfo rootInfo = + resolver.getTypeInfo(AsyncStringCollections.class, AsyncStringCollections.class); + JsonTypeInfo valuesInfo = rootOwner.readFields()[0].readTypeInfo(); + Object initialRootReader = rootInfo.utf8Reader(); + Object initialValues = valuesInfo.utf8Reader(); + JsonTypeInfo writtenValuesInfo = rootOwner.writeFields()[0].writeTypeInfo(); + Object initialRootWriter = rootInfo.utf8Writer(); + Object initialValuesWriter = writtenValuesInfo.utf8Writer(); + String expectedJson = "{\"values\":[" + String.join(",", tokens) + "]}"; + assertEquals( + new String(controlled.json.toJsonBytes(initial), StandardCharsets.UTF_8), expectedJson); + + int pendingTasks = controlled.executor.pendingTasks(); + assertEquals(pendingTasks, 7); + for (int i = 0; i < pendingTasks; i++) { + controlled.executor.runNext(); + boolean initialReaderGraph = + rootInfo.utf8Reader() == initialRootReader && valuesInfo.utf8Reader() == initialValues; + boolean generatedReaderGraph = + rootInfo.utf8Reader() != initialRootReader && valuesInfo.utf8Reader() != initialValues; + assertTrue(initialReaderGraph || generatedReaderGraph); + boolean initialWriterGraph = + rootInfo.utf8Writer() == initialRootWriter + && writtenValuesInfo.utf8Writer() == initialValuesWriter; + boolean generatedWriterGraph = + rootInfo.utf8Writer() != initialRootWriter + && writtenValuesInfo.utf8Writer() != initialValuesWriter; + assertTrue(initialWriterGraph || generatedWriterGraph); + } + + assertNotSame(rootInfo.utf8Reader(), rootOwner); + assertNotSame(valuesInfo.utf8Reader(), initialValues); + assertFinalField(valuesInfo.utf8Reader(), "elementReader", ScalarCodecs.StringCodec.INSTANCE); + assertFinalCollectionFields( + rootInfo.utf8Reader(), Utf8ReaderCodec.class, valuesInfo.utf8Reader()); + assertNotSame(rootInfo.utf8Writer(), initialRootWriter); + assertNotSame(writtenValuesInfo.utf8Writer(), initialValuesWriter); + assertFinalField(writtenValuesInfo.utf8Writer(), "fallback", initialValuesWriter); + assertEquals(writtenValuesInfo.utf8Writer().getClass().getDeclaredFields().length, 1); + assertFinalCollectionFields( + rootInfo.utf8Writer(), Utf8WriterCodec.class, writtenValuesInfo.utf8Writer()); + assertEquals(writeUtf8(writtenValuesInfo.utf8Writer(), null), "null"); + + AsyncStringCollections generated = + controlled.json.fromJson( + input.getBytes(StandardCharsets.UTF_8), AsyncStringCollections.class); + assertEquals(generated.values, expected); + assertEquals( + new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), expectedJson); + generated.values = new LinkedList<>(expected); + assertEquals( + new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), expectedJson); + generated.values = new ArrayList<>(); + assertEquals( + new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), + "{\"values\":[]}"); + generated.values = null; + assertEquals(new String(controlled.json.toJsonBytes(generated), StandardCharsets.UTF_8), "{}"); + for (int size = 0; size <= tokens.length; size++) { + AsyncStringCollections prefix = + controlled.json.fromJson( + stringCollectionInput(tokens, size).getBytes(StandardCharsets.UTF_8), + AsyncStringCollections.class); + assertEquals(prefix.values, expected.subList(0, size)); + } + } + + private static String stringCollectionInput(String[] tokens, int size) { + StringBuilder input = new StringBuilder("{\"values\":["); + for (int i = 0; i < size; i++) { + if (i != 0) { + input.append(i == 10 ? " \n, \t" : ","); + } + input.append(tokens[i]); + } + return input.append("]}").toString(); + } + + @Test + public void nonListCollectionStaysOnOwner() throws Exception { + ControlledJson controlled = controlledJson(); + AsyncFriend first = new AsyncFriend(); + first.id = 1; + AsyncFriend second = new AsyncFriend(); + second.id = 2; + AsyncSetCollections value = new AsyncSetCollections(); + value.values = new LinkedHashSet<>(Arrays.asList(first, second)); + String expected = "{\"values\":[{\"id\":1},{\"id\":2}]}"; + assertEquals(new String(controlled.json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + + JsonTypeResolver resolver = primaryTypeResolver(controlled.json); + ObjectCodec rootOwner = resolver.getObjectCodec(AsyncSetCollections.class); + JsonTypeInfo rootInfo = + resolver.getTypeInfo(AsyncSetCollections.class, AsyncSetCollections.class); + JsonTypeInfo valuesInfo = rootOwner.writeFields()[0].writeTypeInfo(); + Object initialRootWriter = rootInfo.utf8Writer(); + Object initialValuesWriter = valuesInfo.utf8Writer(); + + assertEquals(controlled.executor.pendingTasks(), 10); + controlled.executor.runAll(); + assertNotSame(rootInfo.utf8Writer(), initialRootWriter); + assertSame(valuesInfo.utf8Writer(), initialValuesWriter); + assertFinalCollectionFields( + rootInfo.utf8Writer(), initialValuesWriter.getClass(), initialValuesWriter); + assertEquals(new String(controlled.json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + + @Test + public void utf8ShapeIgnoresPublishedChild() throws Exception { + String input = "{\"creator\":{\"id\":1,\"name\":\"a\"},\"friends\":[{\"id\":2}]}"; + + ControlledJson parentFirst = controlledJson(); + AsyncMixedParent first = + parentFirst.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncMixedParent.class); + parentFirst.executor.runAll(); + assertEquals(first.creator.id, 1); + JsonTypeResolver firstResolver = primaryTypeResolver(parentFirst.json); + JsonTypeInfo firstInfo = + firstResolver.getTypeInfo(AsyncMixedParent.class, AsyncMixedParent.class); + + ControlledJson childFirst = controlledJson(); + childFirst.json.fromJson( + "{\"id\":3,\"name\":\"child\"}".getBytes(StandardCharsets.UTF_8), AsyncCreator.class); + childFirst.executor.runAll(); + AsyncMixedParent second = + childFirst.json.fromJson(input.getBytes(StandardCharsets.UTF_8), AsyncMixedParent.class); + childFirst.executor.runAll(); + assertEquals(second.friends.get(0).id, 2); + JsonTypeResolver secondResolver = primaryTypeResolver(childFirst.json); + JsonTypeInfo secondInfo = + secondResolver.getTypeInfo(AsyncMixedParent.class, AsyncMixedParent.class); + + assertEquals( + declaredFieldShape(firstInfo.utf8Reader()), declaredFieldShape(secondInfo.utf8Reader())); + } + @Test public void nestedAndRecursiveTypes() throws Exception { ControlledJson controlled = controlledJson(); @@ -712,7 +1061,7 @@ public void selfRecursiveWriterUsesThis() throws Exception { assertSame(recursiveField.writeTypeInfo(), typeInfo); assertSame(recursiveField.readTypeInfo(), typeInfo); StringWriterCodec writer = resolver.stringWriter(owner); - assertTrue(writer != owner); + assertNotSame(writer, owner); for (Field field : writer.getClass().getDeclaredFields()) { assertFalse(field.getType() == StringWriterCodec.class, field.toString()); } @@ -759,7 +1108,7 @@ public void nestedPublication() throws Exception { } @Test - public void mutualPublication() throws Exception { + public void mutualTypesUseCanonicalSlots() throws Exception { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); JsonTypeResolver resolver = primaryTypeResolver(json); ObjectCodec firstOwner = resolver.getObjectCodec(MutualFirst.class); @@ -767,37 +1116,16 @@ public void mutualPublication() throws Exception { JsonTypeInfo firstInfo = resolver.getTypeInfo(MutualFirst.class, MutualFirst.class); JsonTypeInfo secondInfo = resolver.getTypeInfo(MutualSecond.class, MutualSecond.class); - Object first = resolver.stringWriter(firstOwner); - Object second = secondInfo.stringWriter(); - assertSame(resolver.stringWriter(secondOwner), second); - assertSame(secondInfo.stringWriter(), second); - assertMutualFields( - firstInfo.stringWriter(), secondInfo.stringWriter(), StringWriterCodec.class); - - first = resolver.utf8Writer(firstOwner); - second = secondInfo.utf8Writer(); - assertSame(resolver.utf8Writer(secondOwner), second); - assertSame(secondInfo.utf8Writer(), second); - assertMutualFields(firstInfo.utf8Writer(), secondInfo.utf8Writer(), Utf8WriterCodec.class); - - first = resolver.latin1Reader(firstOwner); - second = secondInfo.latin1Reader(); - assertSame(resolver.latin1Reader(secondOwner), second); - assertSame(secondInfo.latin1Reader(), second); - assertMutualFields( - firstInfo.latin1Reader(), secondInfo.latin1Reader(), Latin1ReaderCodec.class); - - first = resolver.utf16Reader(firstOwner); - second = secondInfo.utf16Reader(); - assertSame(resolver.utf16Reader(secondOwner), second); - assertSame(secondInfo.utf16Reader(), second); - assertMutualFields(firstInfo.utf16Reader(), secondInfo.utf16Reader(), Utf16ReaderCodec.class); - - first = resolver.utf8Reader(firstOwner); - second = secondInfo.utf8Reader(); - assertSame(resolver.utf8Reader(secondOwner), second); - assertSame(secondInfo.utf8Reader(), second); - assertMutualFields(firstInfo.utf8Reader(), secondInfo.utf8Reader(), Utf8ReaderCodec.class); + assertCyclicSlot(firstInfo.stringWriter(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.stringWriter(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.utf8Writer(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.utf8Writer(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.latin1Reader(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.latin1Reader(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.utf16Reader(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.utf16Reader(), secondOwner, firstInfo); + assertCyclicSlot(firstInfo.utf8Reader(), firstOwner, secondInfo); + assertCyclicSlot(secondInfo.utf8Reader(), secondOwner, firstInfo); } private static void assertPublishedChild( @@ -812,25 +1140,36 @@ private static void assertPublishedChild( assertCapabilityFields(parent, fieldType, child, expectedFields); } - private static void assertMutualFields(Object first, Object second, Class fieldType) - throws Exception { - assertCapabilityFields(first, fieldType, second, 1); - assertCapabilityFields(second, fieldType, first, 1); - } - private static void assertCapabilityFields( Object owner, Class fieldType, Object expected, int expectedFields) throws Exception { int count = 0; for (Field field : owner.getClass().getDeclaredFields()) { if (field.getType() == fieldType) { field.setAccessible(true); - assertSame(field.get(owner), expected, field.toString()); - count++; + if (field.get(owner) == expected) { + count++; + } } } assertEquals(count, expectedFields, owner.getClass().getName()); } + private static void assertCyclicSlot(Object capability, Object owner, JsonTypeInfo target) + throws Exception { + assertNotSame(capability, owner); + int count = 0; + for (Field field : capability.getClass().getDeclaredFields()) { + if (field.getType() == JsonTypeInfo.class) { + field.setAccessible(true); + if (field.get(capability) == target) { + assertTrue(Modifier.isFinal(field.getModifiers())); + count++; + } + } + } + assertEquals(count, 1, capability.getClass().getName()); + } + private static void assertCapabilities(JsonTypeInfo info, Object expected) { assertSame(info.stringWriter(), expected); assertSame(info.utf8Writer(), expected); @@ -839,6 +1178,70 @@ private static void assertCapabilities(JsonTypeInfo info, Object expected) { assertSame(info.utf8Reader(), expected); } + private static List declaredFieldTypes(Object capability) { + List types = new ArrayList<>(); + for (Field field : capability.getClass().getDeclaredFields()) { + types.add(field.getType().getName()); + } + Collections.sort(types); + return types; + } + + private static List declaredFieldShape(Object capability) { + List shape = new ArrayList<>(); + for (Field field : capability.getClass().getDeclaredFields()) { + shape.add( + field.getName() + + ":" + + field.getType().getName() + + ":" + + Modifier.isFinal(field.getModifiers())); + } + Collections.sort(shape); + return shape; + } + + private static void assertFinalField(Object owner, String name, Object expected) + throws Exception { + Field field = owner.getClass().getDeclaredField(name); + field.setAccessible(true); + assertTrue(Modifier.isFinal(field.getModifiers())); + assertSame(field.get(owner), expected); + } + + private static void assertInlineReader(Object[] readers, Object canonical) { + assertNotNull(readers); + assertEquals(readers.length, 1); + assertNotNull(readers[0]); + assertNotSame(readers[0], canonical); + assertSame(readers[0].getClass(), canonical.getClass()); + } + + private static void assertFinalCollectionFields( + Object root, Class fieldType, Object... collections) throws Exception { + int count = 0; + for (Field field : root.getClass().getDeclaredFields()) { + if (field.getType() == fieldType) { + field.setAccessible(true); + Object value = field.get(root); + for (Object collection : collections) { + if (value == collection) { + assertTrue(Modifier.isFinal(field.getModifiers())); + count++; + break; + } + } + } + } + assertEquals(count, collections.length); + } + + private static String writeUtf8(Utf8WriterCodec codec, Object value) { + Utf8JsonWriter writer = JsonTestSupport.newUtf8Writer(); + codec.writeUtf8(writer, value); + return new String(writer.toJsonBytes(), StandardCharsets.UTF_8); + } + private static StringWriterCodec stringWriter( JsonTypeResolver resolver, ObjectCodec owner) { resolver.lockJIT(); @@ -857,8 +1260,9 @@ private static void assertNestedUtf8Readers(ForyJson json) throws Exception { for (Field field : parentReader.getClass().getDeclaredFields()) { if (field.getType() == Utf8ReaderCodec.class) { field.setAccessible(true); - assertSame(field.get(parentReader), childReader); - nestedReaders++; + if (field.get(parentReader) == childReader) { + nestedReaders++; + } } } assertEquals(nestedReaders, 1); @@ -1123,6 +1527,41 @@ public static final class AsyncParent { public List list; } + public static final class AsyncCollections { + public List children; + public List friends; + } + + public static final class AsyncMixedParent { + public AsyncCreator creator; + public List friends; + } + + public static final class AsyncStringCollections { + public List values; + } + + public static final class AsyncSetCollections { + public Set values; + } + + public static final class AsyncCreatorParent { + public final AsyncChild child; + + @JsonCreator({"child"}) + public AsyncCreatorParent(AsyncChild child) { + this.child = child; + } + } + + public static final class AsyncGraphParent { + public AsyncChild child; + } + + public static final class AsyncFriend { + public int id; + } + public static final class AsyncCreator { public final int id; public final String name; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 0c966eeb82..ce64801b73 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -52,6 +52,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Random; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingDeque; @@ -567,6 +568,42 @@ public void readLongArrays() { () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); } + @Test + public void writeLongArrays() { + ForyJson json = newJson(); + LongArrayUtf16Root root = new LongArrayUtf16Root(); + root.text = ZH_TEXT; + long[][] boundaries = { + new long[0], + {0L}, + { + -1L, + 1L, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + (long) Integer.MIN_VALUE - 1L, + (long) Integer.MAX_VALUE + 1L, + Long.MIN_VALUE, + Long.MAX_VALUE + } + }; + for (long[] values : boundaries) { + root.values = values; + assertEquals(new String(json.toJsonBytes(root), StandardCharsets.UTF_8), json.toJson(root)); + } + + Random random = new Random(0x7a11_5eedL); + for (int length = 1; length <= 33; length++) { + long[] values = new long[length]; + for (int i = 0; i < length; i++) { + values[i] = random.nextLong(); + } + root.values = values; + assertEquals(new String(json.toJsonBytes(root), StandardCharsets.UTF_8), json.toJson(root)); + } + assertGeneratedWhenSupported(json, LongArrayUtf16Root.class); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = newJson(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java b/java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java new file mode 100644 index 0000000000..be897bb33d --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/reader/DecimalMathTest.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; + +import java.math.BigInteger; +import java.util.SplittableRandom; +import org.testng.annotations.Test; + +public class DecimalMathTest { + private static final BigInteger UNSIGNED_MASK = + BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE); + + @Test + public void unsignedHighHalf() { + long[] boundaries = { + 0, 1, 2, 0xffff_ffffL, 0x1_0000_0000L, Long.MAX_VALUE, Long.MIN_VALUE, -2, -1 + }; + for (long x : boundaries) { + for (long y : boundaries) { + assertProduct(x, y); + } + } + SplittableRandom random = new SplittableRandom(0x5f3759dfL); + for (int i = 0; i < 10_000; i++) { + assertProduct(random.nextLong(), random.nextLong()); + } + } + + private static void assertProduct(long x, long y) { + BigInteger product = unsigned(x).multiply(unsigned(y)); + assertEquals(DecimalMath.unsignedMultiplyHigh(x, y), product.shiftRight(64).longValue()); + } + + private static BigInteger unsigned(long value) { + return BigInteger.valueOf(value).and(UNSIGNED_MASK); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java index b6755ae4d2..ab03a23df4 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/writer/Jdk25MultiReleaseJarVerifier.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -43,6 +44,9 @@ public final class Jdk25MultiReleaseJarVerifier { private static final String CLASS_NAME = "org.apache.fory.json.writer.BigDecimalFields"; private static final String CLASS_PATH = CLASS_NAME.replace('.', '/') + ".class"; private static final String SOURCE_PATH = CLASS_NAME.replace('.', '/') + ".java"; + private static final String DECIMAL_MATH_NAME = "org.apache.fory.json.reader.DecimalMath"; + private static final String DECIMAL_MATH_PATH = DECIMAL_MATH_NAME.replace('.', '/') + ".class"; + private static final String DECIMAL_MATH_SOURCE = DECIMAL_MATH_NAME.replace('.', '/') + ".java"; private static final String VERSION_PREFIX = "META-INF/versions/25/"; private Jdk25MultiReleaseJarVerifier() {} @@ -63,6 +67,7 @@ public static void main(String[] args) throws Exception { static void verify(Path jarPath, Path sourcesPath) throws Exception { byte[] versionClass; + byte[] decimalMathClass; try (JarFile jar = new JarFile(jarPath.toFile())) { Manifest manifest = jar.getManifest(); require(manifest != null, "missing manifest"); @@ -70,11 +75,16 @@ static void verify(Path jarPath, Path sourcesPath) throws Exception { require("true".equalsIgnoreCase(attributes.getValue("Multi-Release")), "missing manifest"); require(jar.getJarEntry(CLASS_PATH) != null, "missing root BigDecimalFields class"); versionClass = read(jar, VERSION_PREFIX + CLASS_PATH); + require(jar.getJarEntry(DECIMAL_MATH_PATH) != null, "missing root DecimalMath class"); + decimalMathClass = read(jar, VERSION_PREFIX + DECIMAL_MATH_PATH); } try (JarFile sources = new JarFile(sourcesPath.toFile())) { require( sources.getJarEntry(VERSION_PREFIX + SOURCE_PATH) != null, "missing JDK25 BigDecimalFields source"); + require( + sources.getJarEntry(VERSION_PREFIX + DECIMAL_MATH_SOURCE) != null, + "missing JDK25 DecimalMath source"); } Class type = new VersionClassLoader().define(versionClass); @@ -82,6 +92,12 @@ static void verify(Path jarPath, Path sourcesPath) throws Exception { requireVarHandle(type, "INT_COMPACT"); requireVarHandle(type, "INT_VAL"); requireVarHandle(type, "SCALE"); + + Class decimalMath = new VersionClassLoader().define(decimalMathClass); + require(DECIMAL_MATH_NAME.equals(decimalMath.getName()), "wrong JDK25 DecimalMath name"); + Method multiply = decimalMath.getDeclaredMethod("unsignedMultiplyHigh", long.class, long.class); + require(Modifier.isStatic(multiply.getModifiers()), "unsignedMultiplyHigh must be static"); + require(multiply.getReturnType() == long.class, "unsignedMultiplyHigh return type"); } private static void requireVarHandle(Class type, String name) throws NoSuchFieldException {