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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
version: [ 25 ]
vector-length: [ 256, 512 ]
vector-length: [ 128, 256, 512 ]

steps:
- uses: actions/checkout@v4
Expand Down
14 changes: 7 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ tasks.register('downloadTestData', Exec) {
}
}

// Configuration common to ALL Test tasks (including 'test', 'test256', 'test512')
// Configuration common to ALL Test tasks (including 'test', 'test128', 'test256', 'test512')
tasks.withType(Test).configureEach {
dependsOn tasks.named('downloadTestData')

Expand All @@ -138,7 +138,7 @@ tasks.withType(Test).configureEach {
}
}

// --- Test Variants (256/512) ---
// --- Test Variants (128/256/512) ---
test {
useJUnitPlatform()
jvmArgs += [
Expand All @@ -147,15 +147,14 @@ test {
failOnNoDiscoveredTests = false
}

// Generate test tasks for specific species (256, 512)
[256, 512].each { species ->
// Generate test tasks for specific species (128, 256, 512)
[128, 256, 512].each { species ->
tasks.register("test${species}", Test) {
group = 'verification'
description = "Runs tests with org.simdjson.species=${species}"

// Fix: Removed 'dependsOn test'. This allows test256 to run independently.
// We use mustRunAfter so they don't interleave output if run together via 'check'.
dependsOn tasks.named('test')
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath

useJUnitPlatform()
jvmArgs += [
Expand All @@ -167,6 +166,7 @@ test {
}

tasks.named('check') {
dependsOn tasks.named('test128')
dependsOn tasks.named('test256')
dependsOn tasks.named('test512')
}
Expand Down
170 changes: 169 additions & 1 deletion src/main/java/org/simdjson/StructuralIndexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import java.util.Arrays;

import static jdk.incubator.vector.ByteVector.SPECIES_128;
import static jdk.incubator.vector.ByteVector.SPECIES_256;
import static jdk.incubator.vector.ByteVector.SPECIES_512;
import static jdk.incubator.vector.VectorOperators.ULE;
Expand Down Expand Up @@ -42,12 +43,179 @@ class StructuralIndexer {
void index(byte[] buffer, int length) {
bitIndexes.reset();
switch (VECTOR_BIT_SIZE) {
case 128 -> index128(buffer, length);
case 256 -> index256(buffer, length);
case 512 -> index512(buffer, length);
default -> throw new UnsupportedOperationException("Unsupported vector width: " + VECTOR_BIT_SIZE * 64);
default -> throw new UnsupportedOperationException("Unsupported vector width: " + VECTOR_BIT_SIZE);
}
}

/**
* Stage-1 indexer for 128-bit vectors: four 16-byte lanes per 64-byte block (same layout as simdjson ARM64
* {@code simd8x64} / {@code index<64>}).
*/
private void index128(byte[] buffer, int length) {
long prevInString = 0;
long prevEscaped = 0;
long prevStructurals = 0;
long unescapedCharsError = 0;
long prevScalar = 0;

int loopBound = SPECIES_512.loopBound(length);
int offset = 0;
int blockIndex = 0;
for (; offset < loopBound; offset += STEP_SIZE) {
ByteVector chunk0 = ByteVector.fromArray(SPECIES_128, buffer, offset);
ByteVector chunk1 = ByteVector.fromArray(SPECIES_128, buffer, offset + 16);
ByteVector chunk2 = ByteVector.fromArray(SPECIES_128, buffer, offset + 32);
ByteVector chunk3 = ByteVector.fromArray(SPECIES_128, buffer, offset + 48);

long backslash = pack128(
chunk0.eq(BACKSLASH).toLong(),
chunk1.eq(BACKSLASH).toLong(),
chunk2.eq(BACKSLASH).toLong(),
chunk3.eq(BACKSLASH).toLong());

long escaped;
if (backslash == 0) {
escaped = prevEscaped;
prevEscaped = 0;
} else {
backslash &= ~prevEscaped;
long followsEscape = backslash << 1 | prevEscaped;
long oddSequenceStarts = backslash & ODD_BITS_MASK & ~followsEscape;

long sequencesStartingOnEvenBits = oddSequenceStarts + backslash;
prevEscaped = ((oddSequenceStarts >>> 1) + (backslash >>> 1) + ((oddSequenceStarts & backslash) & 1)) >>> 63;

long invertMask = sequencesStartingOnEvenBits << 1;
escaped = (EVEN_BITS_MASK ^ invertMask) & followsEscape;
}

long unescaped = pack128(
chunk0.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
chunk1.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
chunk2.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
chunk3.compare(ULE, LAST_CONTROL_CHARACTER).toLong());

long quote0 = chunk0.eq(QUOTE).toLong();
long quote1 = chunk1.eq(QUOTE).toLong();
long quote2 = chunk2.eq(QUOTE).toLong();
long quote3 = chunk3.eq(QUOTE).toLong();
long quote = pack128(quote0, quote1, quote2, quote3) & ~escaped;

long inString = prefixXor(quote) ^ prevInString;
prevInString = inString >> 63;

VectorShuffle<Byte> chunk0Low = chunk0.and(LOW_NIBBLE_MASK).toShuffle();
VectorShuffle<Byte> chunk1Low = chunk1.and(LOW_NIBBLE_MASK).toShuffle();
VectorShuffle<Byte> chunk2Low = chunk2.and(LOW_NIBBLE_MASK).toShuffle();
VectorShuffle<Byte> chunk3Low = chunk3.and(LOW_NIBBLE_MASK).toShuffle();

long whitespace = pack128(
chunk0.eq(WHITESPACE_TABLE.rearrange(chunk0Low)).toLong(),
chunk1.eq(WHITESPACE_TABLE.rearrange(chunk1Low)).toLong(),
chunk2.eq(WHITESPACE_TABLE.rearrange(chunk2Low)).toLong(),
chunk3.eq(WHITESPACE_TABLE.rearrange(chunk3Low)).toLong());

long op = pack128(
chunk0.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk0Low)).toLong(),
chunk1.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk1Low)).toLong(),
chunk2.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk2Low)).toLong(),
chunk3.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk3Low)).toLong());

long scalar = ~(op | whitespace);
long nonQuoteScalar = scalar & ~quote;
long followsNonQuoteScalar = nonQuoteScalar << 1 | prevScalar;
prevScalar = nonQuoteScalar >>> 63;
long potentialScalarStart = scalar & ~followsNonQuoteScalar;
long potentialStructuralStart = op | potentialScalarStart;
bitIndexes.write(blockIndex, prevStructurals);
blockIndex += STEP_SIZE;
prevStructurals = potentialStructuralStart & ~(inString ^ quote);
unescapedCharsError |= unescaped & inString;
}

byte[] remainder = remainder(buffer, length, blockIndex);
ByteVector chunk0 = ByteVector.fromArray(SPECIES_128, remainder, 0);
ByteVector chunk1 = ByteVector.fromArray(SPECIES_128, remainder, 16);
ByteVector chunk2 = ByteVector.fromArray(SPECIES_128, remainder, 32);
ByteVector chunk3 = ByteVector.fromArray(SPECIES_128, remainder, 48);

long backslash = pack128(
chunk0.eq(BACKSLASH).toLong(),
chunk1.eq(BACKSLASH).toLong(),
chunk2.eq(BACKSLASH).toLong(),
chunk3.eq(BACKSLASH).toLong());

long escaped;
if (backslash == 0) {
escaped = prevEscaped;
} else {
backslash &= ~prevEscaped;
long followsEscape = backslash << 1 | prevEscaped;
long oddSequenceStarts = backslash & ODD_BITS_MASK & ~followsEscape;

long sequencesStartingOnEvenBits = oddSequenceStarts + backslash;
long invertMask = sequencesStartingOnEvenBits << 1;
escaped = (EVEN_BITS_MASK ^ invertMask) & followsEscape;
}

long unescaped = pack128(
chunk0.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
chunk1.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
chunk2.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
chunk3.compare(ULE, LAST_CONTROL_CHARACTER).toLong());

long quote = pack128(
chunk0.eq(QUOTE).toLong(),
chunk1.eq(QUOTE).toLong(),
chunk2.eq(QUOTE).toLong(),
chunk3.eq(QUOTE).toLong()) & ~escaped;

long inString = prefixXor(quote) ^ prevInString;
prevInString = inString >> 63;

VectorShuffle<Byte> chunk0Low = chunk0.and(LOW_NIBBLE_MASK).toShuffle();
VectorShuffle<Byte> chunk1Low = chunk1.and(LOW_NIBBLE_MASK).toShuffle();
VectorShuffle<Byte> chunk2Low = chunk2.and(LOW_NIBBLE_MASK).toShuffle();
VectorShuffle<Byte> chunk3Low = chunk3.and(LOW_NIBBLE_MASK).toShuffle();

long whitespace = pack128(
chunk0.eq(WHITESPACE_TABLE.rearrange(chunk0Low)).toLong(),
chunk1.eq(WHITESPACE_TABLE.rearrange(chunk1Low)).toLong(),
chunk2.eq(WHITESPACE_TABLE.rearrange(chunk2Low)).toLong(),
chunk3.eq(WHITESPACE_TABLE.rearrange(chunk3Low)).toLong());

long op = pack128(
chunk0.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk0Low)).toLong(),
chunk1.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk1Low)).toLong(),
chunk2.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk2Low)).toLong(),
chunk3.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk3Low)).toLong());

long scalar = ~(op | whitespace);
long nonQuoteScalar = scalar & ~quote;
long followsNonQuoteScalar = nonQuoteScalar << 1 | prevScalar;
long potentialScalarStart = scalar & ~followsNonQuoteScalar;
long potentialStructuralStart = op | potentialScalarStart;
bitIndexes.write(blockIndex, prevStructurals);
blockIndex += STEP_SIZE;
prevStructurals = potentialStructuralStart & ~(inString ^ quote);
unescapedCharsError |= unescaped & inString;
bitIndexes.write(blockIndex, prevStructurals);
bitIndexes.finish();
if (prevInString != 0) {
throw new JsonParsingException("Unclosed string. A string is opened, but never closed.");
}
if (unescapedCharsError != 0) {
throw new JsonParsingException("Unescaped characters. Within strings, there are characters that should be escaped.");
}
}

private static long pack128(long mask0, long mask1, long mask2, long mask3) {
return mask0 | (mask1 << 16) | (mask2 << 32) | (mask3 << 48);
}

private void index256(byte[] buffer, int length) {
long prevInString = 0;
long prevEscaped = 0;
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/org/simdjson/VectorUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ class VectorUtils {
BYTE_SPECIES = ByteVector.SPECIES_256;
INT_SPECIES = IntVector.SPECIES_256;
}
case "128" -> {
BYTE_SPECIES = ByteVector.SPECIES_128;
INT_SPECIES = IntVector.SPECIES_128;
}
default -> throw new IllegalArgumentException("Unsupported vector species: " + species);
}
}

private static void assertSupportForSpecies(VectorSpecies<?> species) {
if (species.vectorShape() != VectorShape.S_256_BIT && species.vectorShape() != VectorShape.S_512_BIT) {
VectorShape shape = species.vectorShape();
if (shape != VectorShape.S_128_BIT && shape != VectorShape.S_256_BIT && shape != VectorShape.S_512_BIT) {
throw new IllegalArgumentException("Unsupported vector species: " + species);
}
}
Expand Down