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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/languages/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ Load this file when changing anything under `java/` or when Java drives a cross-
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.
- Generated Latin1 and UTF-8 JSON readers classify arbitrary-order known fields by a bounded raw
prefix and verify the complete compile-time field token in the generated slow owner. A prefix or
token miss must leave the name unread and use the existing hash/table path for escapes, aliases,
unknown names, collisions, and malformed input. Keep Any-property and UTF-16 readers on their
existing hash paths. Do not extract the classifier into an independent scanner owner, add a
declaration-order assumption, or replace complete token verification with prefix equality.
- Generated UTF-8 object writers own their C2 boundaries in their actual emitted bytecode. A split
writer keeps object framing and the final declaration-order field range in public `writeUtf8`;
every preceding range is a direct private final helper. Cold source generation compiles each
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,11 +630,10 @@ private ArrayList<Object> readLatin1ArrayList(Latin1JsonReader reader) {
list.add(e3);
return list;
}
return readLatin1ArrayListTail(reader, e0, e1, e2, e3);
}

private ArrayList<Object> readLatin1ArrayListTail(
Latin1JsonReader reader, Object e0, Object e1, Object e2, Object e3) {
// Keep this real exact-allocation prefix in the collection owner. Splitting here makes each
// method smaller than C2's hot-inline limit, so a generated caller can absorb the collection
// and element closure solely according to compilation order. The uncommon longer tail stays
// separate below.
Object e4 = readLatin1Element(reader);
if (!reader.consumeNextCommaOrEndArray()) {
reader.exitDepth();
Expand Down Expand Up @@ -1389,7 +1388,9 @@ private ArrayList<Object> readLatin1ArrayListLongTail(
list.add(e7);
return list;
}
reader.reserveGraphMemory(ARRAY_LIST_OWNER_BYTES + 8 * REFERENCE_BYTES);
// Capacity nine is materialized before the ninth child is read, so charge every backing
// slot before allocating the list.
reader.reserveGraphMemory(ARRAY_LIST_OWNER_BYTES + 9 * REFERENCE_BYTES);
ArrayList<Object> list = new ArrayList<>(9);
list.add(e0);
list.add(e1);
Expand All @@ -1399,14 +1400,15 @@ private ArrayList<Object> readLatin1ArrayListLongTail(
list.add(e5);
list.add(e6);
list.add(e7);
list.add(codec.readLatin1(reader));
int pendingSize = 0;
do {
while (reader.consumeNextCommaOrEndArray()) {
if ((pendingSize & REFERENCE_BATCH_MASK) == REFERENCE_BATCH_MASK) {
reader.reserveGraphMemory(REFERENCE_BATCH_BYTES);
}
list.add(codec.readLatin1(reader));
pendingSize++;
} while (reader.consumeNextCommaOrEndArray());
}
int tailSize = pendingSize & REFERENCE_BATCH_MASK;
if (tailSize != 0) {
reader.reserveGraphMemory(tailSize * REFERENCE_BYTES);
Expand Down Expand Up @@ -1538,7 +1540,9 @@ private ArrayList<Object> readUtf16ArrayListLongTail(
list.add(e7);
return list;
}
reader.reserveGraphMemory(ARRAY_LIST_OWNER_BYTES + 8 * REFERENCE_BYTES);
// Capacity nine is materialized before the ninth child is read, so charge every backing
// slot before allocating the list.
reader.reserveGraphMemory(ARRAY_LIST_OWNER_BYTES + 9 * REFERENCE_BYTES);
ArrayList<Object> list = new ArrayList<>(9);
list.add(e0);
list.add(e1);
Expand All @@ -1548,14 +1552,15 @@ private ArrayList<Object> readUtf16ArrayListLongTail(
list.add(e5);
list.add(e6);
list.add(e7);
list.add(codec.readUtf16(reader));
int pendingSize = 0;
do {
while (reader.consumeNextCommaOrEndArray()) {
if ((pendingSize & REFERENCE_BATCH_MASK) == REFERENCE_BATCH_MASK) {
reader.reserveGraphMemory(REFERENCE_BATCH_BYTES);
}
list.add(codec.readUtf16(reader));
pendingSize++;
} while (reader.consumeNextCommaOrEndArray());
}
int tailSize = pendingSize & REFERENCE_BATCH_MASK;
if (tailSize != 0) {
reader.reserveGraphMemory(tailSize * REFERENCE_BYTES);
Expand Down Expand Up @@ -1611,16 +1616,9 @@ private ArrayList<Object> readUtf8ArrayList(
list.add(e3);
return list;
}
return readUtf8ArrayListTail(reader, codec, e0, e1, e2, e3);
}

private ArrayList<Object> readUtf8ArrayListTail(
Utf8JsonReader reader,
Utf8ReaderCodec<Object> codec,
Object e0,
Object e1,
Object e2,
Object e3) {
// Keep the fifth exact-allocation lane in the collection owner. If this lane is split after
// four elements, both resulting methods fall below C2's hot-inline limit and let an outer
// fallback caller absorb the object-element closure according to compilation order.
Object e4 = codec.readUtf8(reader);
if (!reader.consumeNextCommaOrEndArray()) {
reader.exitDepth();
Expand All @@ -1633,6 +1631,17 @@ private ArrayList<Object> readUtf8ArrayListTail(
list.add(e4);
return list;
}
return readUtf8ArrayListTail(reader, codec, e0, e1, e2, e3, e4);
}

private ArrayList<Object> readUtf8ArrayListTail(
Utf8JsonReader reader,
Utf8ReaderCodec<Object> codec,
Object e0,
Object e1,
Object e2,
Object e3,
Object e4) {
Object e5 = codec.readUtf8(reader);
if (!reader.consumeNextCommaOrEndArray()) {
reader.exitDepth();
Expand Down Expand Up @@ -1687,7 +1696,9 @@ private ArrayList<Object> readUtf8ArrayListLongTail(
list.add(e7);
return list;
}
reader.reserveGraphMemory(ARRAY_LIST_OWNER_BYTES + 8 * REFERENCE_BYTES);
// Capacity nine is materialized before the ninth child is read, so charge every backing
// slot before allocating the list.
reader.reserveGraphMemory(ARRAY_LIST_OWNER_BYTES + 9 * REFERENCE_BYTES);
ArrayList<Object> list = new ArrayList<>(9);
list.add(e0);
list.add(e1);
Expand All @@ -1697,14 +1708,15 @@ private ArrayList<Object> readUtf8ArrayListLongTail(
list.add(e5);
list.add(e6);
list.add(e7);
list.add(codec.readUtf8(reader));
int pendingSize = 0;
do {
while (reader.consumeNextCommaOrEndArray()) {
if ((pendingSize & REFERENCE_BATCH_MASK) == REFERENCE_BATCH_MASK) {
reader.reserveGraphMemory(REFERENCE_BATCH_BYTES);
}
list.add(codec.readUtf8(reader));
pendingSize++;
} while (reader.consumeNextCommaOrEndArray());
}
int tailSize = pendingSize & REFERENCE_BATCH_MASK;
if (tailSize != 0) {
reader.reserveGraphMemory(tailSize * REFERENCE_BYTES);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ boolean directSlowFieldIndex() {
return false;
}

boolean rawFieldNameDispatch() {
return false;
}

abstract boolean isDirectName(String name, boolean tokenValueRead);

abstract Expression tryReadNextFieldNameColon(JsonFieldInfo property, boolean tokenValueRead);
Expand Down Expand Up @@ -2884,8 +2888,8 @@ private Expression slowReadFromFirstExpression(
expressions.add(hashes);
expressions.add(fieldIndex);
Expression anyMapCreated = anyMapCreatedFlag(expressions);
expressions.add(expectExpr(':'));
Expression.ListExpression loop = new Expression.ListExpression();
loop.add(expectExpr(':'));
loop.add(
fieldSwitch(
builder,
Expand All @@ -2912,16 +2916,21 @@ private Expression slowReadFromFirstExpression(
if (fieldStart != null) {
loop.add(fieldStart);
}
Expression fieldHash = readFieldNameHash("fieldHash");
loop.add(fieldHash);
loop.add(assignSlowFieldIndex(fieldIndex, expectedIndex, hashes, fieldHash, properties));
if (any != null) {
loop.add(
new Expression.Assign(
new Reference("firstFieldHash", TypeRef.of(long.class)), fieldHash));
loop.add(
new Expression.Assign(
new Reference("firstFieldStart", TypeRef.of(int.class)), fieldStart));
if (any == null && rawFieldNameDispatch() && hasDirectFieldName(properties)) {
loop.add(readDirectFieldIndex(fieldIndex, expectedIndex, hashes, properties));
} else {
Expression fieldHash = readFieldNameHash("fieldHash");
loop.add(fieldHash);
loop.add(assignSlowFieldIndex(fieldIndex, expectedIndex, hashes, fieldHash, properties));
loop.add(expectExpr(':'));
if (any != null) {
loop.add(
new Expression.Assign(
new Reference("firstFieldHash", TypeRef.of(long.class)), fieldHash));
loop.add(
new Expression.Assign(
new Reference("firstFieldStart", TypeRef.of(int.class)), fieldStart));
}
}
expressions.add(new Expression.While(Expression.Literal.True, loop));
return expressions;
Expand All @@ -2935,6 +2944,9 @@ private Expression readNextHashedField(
Expression hashes,
Expression expectedIndex,
Expression anyMapCreated) {
if (any == null && rawFieldNameDispatch() && hasDirectFieldName(properties)) {
return readNextDirectField(builder, type, properties, object, hashes, expectedIndex);
}
Expression fieldStart =
any == null
? null
Expand All @@ -2958,6 +2970,98 @@ private Expression readNextHashedField(
return expressions;
}

private Expression readNextDirectField(
JsonGeneratedCodecBuilder builder,
Class<?> type,
JsonFieldInfo[] properties,
Expression object,
Expression hashes,
Expression expectedIndex) {
Reference fieldIndex = new Reference("fieldIndex", TypeRef.of(int.class));
return new Expression.ListExpression(
new Expression.Variable("fieldIndex", Expression.Literal.ofInt(JsonFieldTable.UNKNOWN)),
readDirectFieldIndex(fieldIndex, expectedIndex, hashes, properties),
fieldSwitch(builder, type, properties, object, fieldIndex),
updateExpectedIndex(expectedIndex, fieldIndex));
}

private Expression readDirectFieldIndex(
Expression fieldIndex,
Expression expectedIndex,
Expression hashes,
JsonFieldInfo[] properties) {
int unresolved = JsonFieldTable.UNKNOWN;
Expression prefix =
new Expression.Invoke(
readerRef(), "readFieldNamePrefix", "fieldPrefix", TypeRef.of(int.class), false);
Expression fieldHash = readFieldNameHash("fieldHash");
Expression fallback =
new Expression.ListExpression(
fieldHash,
assignSlowFieldIndex(fieldIndex, expectedIndex, hashes, fieldHash, properties),
expectExpr(':'));
// Keep classification and complete token verification in the generated slow owner. On a miss
// the token matcher leaves the name unread, so one existing hash path retains every escaped,
// aliased, unknown, and malformed-name behavior without adding a second scanner owner.
return new Expression.ListExpression(
prefix,
new Expression.Assign(fieldIndex, Expression.Literal.ofInt(unresolved)),
directFieldNameSwitch(fieldIndex, prefix, properties),
new Expression.If(eq(fieldIndex, Expression.Literal.ofInt(unresolved)), fallback));
}

private boolean hasDirectFieldName(JsonFieldInfo[] properties) {
for (JsonFieldInfo property : properties) {
if (!property.name().isEmpty() && isDirectName(property.name(), true)) {
return true;
}
}
return false;
}

private Expression directFieldNameSwitch(
Expression fieldIndex, Expression prefix, JsonFieldInfo[] properties) {
int[] keys = new int[properties.length];
int keyCount = 0;
for (JsonFieldInfo property : properties) {
if (property.name().isEmpty() || !isDirectName(property.name(), true)) {
continue;
}
int key = (int) JsonAsciiToken.prefix(fieldNameToken(property.name()));
boolean found = false;
for (int i = 0; i < keyCount; i++) {
if (keys[i] == key) {
found = true;
break;
}
}
if (!found) {
keys[keyCount++] = key;
}
}
Expression.Switch.Case[] cases = new Expression.Switch.Case[keyCount];
for (int keyIndex = 0; keyIndex < keyCount; keyIndex++) {
int key = keys[keyIndex];
Expression resolve = new Expression.Empty();
for (int field = properties.length - 1; field >= 0; field--) {
JsonFieldInfo property = properties[field];
if (!property.name().isEmpty()
&& isDirectName(property.name(), true)
&& (int) JsonAsciiToken.prefix(fieldNameToken(property.name())) == key) {
resolve =
new Expression.If(
tryReadNextFieldNameColon(property, true),
new Expression.Assign(fieldIndex, Expression.Literal.ofInt(field)),
resolve);
}
}
cases[keyIndex] =
new Expression.Switch.Case(
key, new Expression.ListExpression(resolve, new Expression.Break()));
}
return new Expression.Switch(prefix, cases, null);
}

private Expression fieldSwitch(
JsonGeneratedCodecBuilder builder,
Class<?> type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ boolean directSlowFieldIndex() {
return true;
}

@Override
boolean rawFieldNameDispatch() {
return true;
}

@Override
boolean isDirectName(String name, boolean tokenValueRead) {
return JsonAsciiToken.isLongPackable(fieldNameToken(name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ String readFieldMethod() {
return "readUtf8";
}

@Override
boolean directSlowFieldIndex() {
return true;
}

@Override
boolean rawFieldNameDispatch() {
return true;
}

@Override
Expression consumeCommaOrEndObjectExpr() {
Expression comma =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2184,6 +2185,23 @@ public long readFieldNameHash() {
return readQuotedStringHash();
}

/**
* Returns the raw four-byte prefix at the next field name after consuming legal whitespace.
*
* <p>Generated object readers use this only as a discriminator before a complete field-token
* check. A miss leaves the name unread so the ordinary hash parser retains escape, Unicode,
* alias, unknown-field, and malformed-input handling.
*/
@Internal
public int readFieldNamePrefix() {
skipWhitespaceFast();
int offset = position;
if (offset <= input.length - Integer.BYTES) {
return LittleEndian.getInt32(input, offset);
}
return 0;
}

public boolean tryReadFieldNameColon(long expectedHash, long expectedMask, int expectedLength) {
int mark = position;
skipWhitespaceFast();
Expand Down
Loading
Loading