initial ? wtf

This commit is contained in:
Relism
2026-03-15 01:15:43 +01:00
commit 58d8c94ea7
52 changed files with 5260 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.relism</groupId>
<artifactId>fastpathrouter-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>fpr-core</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,15 @@
package dev.relism.fpr.core;
public interface ByteView {
int length();
byte byteAt(int index);
default boolean supportsLong() {
return false;
}
default long longAt(int index) {
throw new UnsupportedOperationException("longAt not supported");
}
}
@@ -0,0 +1,10 @@
package dev.relism.fpr.core;
/**
* Immutable router interface for hot-path matching.
*/
public interface FastPathRouter<I, H> {
int NO_MATCH = -1;
int match(I input, MatchResult<H> out);
}
@@ -0,0 +1,211 @@
package dev.relism.fpr.core;
import lombok.Setter;
/**
* Reusable match result container for parameter spans.
*/
public final class MatchResult<H> {
private final int[] keyIds;
private final int[] starts;
private final int[] lens;
private final int[] stackState;
private final int[] stackSegStart;
private final int[] stackSegLen;
private final int[] stackNextIdx;
private final int[] stackParamMark;
private final int[] stackEdgeIndex;
private final byte[] stackKind;
private final int[] scratchKeyIds;
private final int[] scratchStarts;
private final int[] scratchLens;
private final int[] scratchEdges;
private int stackSize;
private int paramCount;
@Setter
private H handler;
private int labelId;
public MatchResult() {
this(8, 32);
}
public MatchResult(int maxParams) {
this(maxParams, Math.max(32, maxParams));
}
public MatchResult(int maxParams, int maxStack) {
if (maxParams < 0) {
throw new IllegalArgumentException("maxParams must be >= 0");
}
if (maxStack <= 0) {
throw new IllegalArgumentException("maxStack must be > 0");
}
this.keyIds = new int[maxParams];
this.starts = new int[maxParams];
this.lens = new int[maxParams];
this.stackState = new int[maxStack];
this.stackSegStart = new int[maxStack];
this.stackSegLen = new int[maxStack];
this.stackNextIdx = new int[maxStack];
this.stackParamMark = new int[maxStack];
this.stackEdgeIndex = new int[maxStack];
this.stackKind = new byte[maxStack];
this.scratchKeyIds = new int[maxParams];
this.scratchStarts = new int[maxParams];
this.scratchLens = new int[maxParams];
this.scratchEdges = new int[maxStack];
}
/**
* Clears handler and params but keeps label id.
*/
public MatchResult<H> reset() {
this.paramCount = 0;
this.stackSize = 0;
this.handler = null;
return this;
}
public MatchResult<H> labelId(int labelId) {
this.labelId = labelId;
return this;
}
public int labelId() {
return labelId;
}
public int paramCount() {
return paramCount;
}
/**
* Iterates params without allocations using a precomputed name array.
* Use {@link RouterBuilder#paramNames()} to obtain the name array.
*/
public void forEachParam(ByteView view, String[] paramNames, ParamConsumer consumer) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (paramNames == null) {
throw new IllegalArgumentException("paramNames must not be null");
}
if (consumer == null) {
throw new IllegalArgumentException("consumer must not be null");
}
for (int i = 0; i < paramCount; i++) {
int keyId = keyIds[i];
if (keyId < 0 || keyId >= paramNames.length) {
throw new IllegalArgumentException("param name missing for keyId " + keyId);
}
consumer.accept(paramNames[keyId], view, starts[i], lens[i]);
}
}
public int keyIdAt(int index) {
return keyIds[index];
}
public int startAt(int index) {
return starts[index];
}
public int lenAt(int index) {
return lens[index];
}
public H handler() {
return handler;
}
public int mark() {
return paramCount;
}
public void rollbackTo(int mark) {
paramCount = mark;
}
public void resetTo(int mark) {
paramCount = mark;
}
public void addParam(int keyId, int start, int len) {
if (paramCount >= keyIds.length) {
throw new IllegalStateException("MatchResult capacity exceeded");
}
keyIds[paramCount] = keyId;
starts[paramCount] = start;
lens[paramCount] = len;
paramCount++;
}
int[] stackStateArray() {
return stackState;
}
int[] stackSegStartArray() {
return stackSegStart;
}
int[] stackSegLenArray() {
return stackSegLen;
}
int[] stackNextIdxArray() {
return stackNextIdx;
}
int[] stackParamMarkArray() {
return stackParamMark;
}
int[] stackEdgeIndexArray() {
return stackEdgeIndex;
}
byte[] stackKindArray() {
return stackKind;
}
int stackSize() {
return stackSize;
}
void stackSize(int size) {
this.stackSize = size;
}
int[] keyIdsArray() {
return keyIds;
}
int[] startsArray() {
return starts;
}
int[] lensArray() {
return lens;
}
int[] scratchKeyIdsArray() {
return scratchKeyIds;
}
int[] scratchStartsArray() {
return scratchStarts;
}
int[] scratchLensArray() {
return scratchLens;
}
int[] scratchEdgesArray() {
return scratchEdges;
}
void paramCount(int count) {
this.paramCount = count;
}
}
@@ -0,0 +1,77 @@
package dev.relism.fpr.core;
/**
* Internal access bridge for match-time scratch storage.
*/
public final class MatchResultAccess {
private MatchResultAccess() {
}
public static int[] stackState(MatchResult<?> result) {
return result.stackStateArray();
}
public static int[] stackSegStart(MatchResult<?> result) {
return result.stackSegStartArray();
}
public static int[] stackSegLen(MatchResult<?> result) {
return result.stackSegLenArray();
}
public static int[] stackNextIdx(MatchResult<?> result) {
return result.stackNextIdxArray();
}
public static int[] stackParamMark(MatchResult<?> result) {
return result.stackParamMarkArray();
}
public static int[] stackEdgeIndex(MatchResult<?> result) {
return result.stackEdgeIndexArray();
}
public static byte[] stackKind(MatchResult<?> result) {
return result.stackKindArray();
}
public static int[] keyIds(MatchResult<?> result) {
return result.keyIdsArray();
}
public static int[] starts(MatchResult<?> result) {
return result.startsArray();
}
public static int[] lens(MatchResult<?> result) {
return result.lensArray();
}
public static int[] scratchKeyIds(MatchResult<?> result) {
return result.scratchKeyIdsArray();
}
public static int[] scratchStarts(MatchResult<?> result) {
return result.scratchStartsArray();
}
public static int[] scratchLens(MatchResult<?> result) {
return result.scratchLensArray();
}
public static int[] scratchEdges(MatchResult<?> result) {
return result.scratchEdgesArray();
}
public static int stackSize(MatchResult<?> result) {
return result.stackSize();
}
public static void stackSize(MatchResult<?> result, int size) {
result.stackSize(size);
}
public static void paramCount(MatchResult<?> result, int count) {
result.paramCount(count);
}
}
@@ -0,0 +1,9 @@
package dev.relism.fpr.core;
@FunctionalInterface
public interface ParamConsumer {
/**
* Receives a param name and its byte span in the matched input.
*/
void accept(String name, ByteView view, int start, int len);
}
@@ -0,0 +1,153 @@
package dev.relism.fpr.core;
import java.util.Arrays;
import java.util.List;
/**
* Immutable route pattern built from segment tokens.
*/
public final class RoutePattern {
private final List<Segment> segments;
RoutePattern(List<Segment> segments) {
this.segments = segments;
}
public List<Segment> segments() {
return segments;
}
public static RoutePattern of(Segment... segments) {
return new RoutePattern(Arrays.asList(segments));
}
public static RoutePattern fromSegments(List<Segment> segments) {
return new RoutePattern(segments);
}
public static Literal literal(String text) {
return new Literal(text);
}
public static Param param(String name) {
return new Param(name);
}
public static Wildcard wildcard() {
return new Wildcard();
}
public static CatchAll catchAll() {
return new CatchAll(null);
}
public static CatchAll catchAll(String name) {
return new CatchAll(name);
}
public static Mixed mixed(String[] literals, String[] params) {
return new Mixed(literals, params);
}
public enum SegmentType {
LITERAL,
PARAM,
WILDCARD,
CATCH_ALL,
MIXED
}
public interface Segment {
SegmentType type();
}
public static final class Literal implements Segment {
private final String text;
public Literal(String text) {
if (text == null || text.isEmpty()) {
throw new IllegalArgumentException("literal must be non-empty");
}
this.text = text;
}
public String text() {
return text;
}
@Override
public SegmentType type() {
return SegmentType.LITERAL;
}
}
public static final class Param implements Segment {
private final String name;
public Param(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("param name must be non-empty");
}
this.name = name;
}
public String name() {
return name;
}
@Override
public SegmentType type() {
return SegmentType.PARAM;
}
}
public static final class Wildcard implements Segment {
@Override
public SegmentType type() {
return SegmentType.WILDCARD;
}
}
public static final class CatchAll implements Segment {
private final String name;
public CatchAll(String name) {
this.name = name;
}
public String name() {
return name;
}
@Override
public SegmentType type() {
return SegmentType.CATCH_ALL;
}
}
public static final class Mixed implements Segment {
private final String[] literals;
private final String[] params;
public Mixed(String[] literals, String[] params) {
if (literals == null || params == null || literals.length != params.length + 1) {
throw new IllegalArgumentException("mixed segment requires literals=paramCount+1");
}
this.literals = literals;
this.params = params;
}
public String[] literals() {
return literals;
}
public String[] params() {
return params;
}
@Override
public SegmentType type() {
return SegmentType.MIXED;
}
}
}
@@ -0,0 +1,144 @@
package dev.relism.fpr.core;
import dev.relism.fpr.core.internal.compile.RouteCompiler;
import lombok.Getter;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Builder for creating and compiling immutable routers.
*/
public final class RouterBuilder<H> {
private final List<RouteSpec<H>> routes = new ArrayList<>();
private final LinkedHashMap<String, Integer> paramIds = new LinkedHashMap<>();
private final List<String> paramNames = new ArrayList<>();
private final LinkedHashMap<String, Integer> labelIds = new LinkedHashMap<>();
public RouterBuilder<H> add(RoutePattern pattern, H handler) {
return add(0, pattern, handler);
}
public RouterBuilder<H> add(String label, RoutePattern pattern, H handler) {
return add(labelId(label), pattern, handler);
}
public RouterBuilder<H> add(Enum<?> label, RoutePattern pattern, H handler) {
return add(labelId(label), pattern, handler);
}
public RouterBuilder<H> add(String[] labels, RoutePattern pattern, H handler) {
if (labels == null || labels.length == 0) {
return add(0, pattern, handler);
}
for (String label : labels) {
add(labelId(label), pattern, handler);
}
return this;
}
public RouterBuilder<H> add(Enum<?>[] labels, RoutePattern pattern, H handler) {
if (labels == null || labels.length == 0) {
return add(0, pattern, handler);
}
for (Enum<?> label : labels) {
add(labelId(label), pattern, handler);
}
return this;
}
public RouterBuilder<H> add(int labelId, RoutePattern pattern, H handler) {
if (pattern == null) {
throw new IllegalArgumentException("pattern must not be null");
}
if (labelId < 0) {
throw new IllegalArgumentException("labelId must be >= 0");
}
registerParams(pattern);
routes.add(new RouteSpec<>(labelId, pattern, handler, routes.size()));
return this;
}
public int maxParamCount() {
return paramIds.size();
}
/**
* Returns param names by key id order for optional zero-copy extraction.
* Cache the returned array for reuse; it is stable after route registration.
*/
public String[] paramNames() {
return paramNames.toArray(new String[0]);
}
public int labelId(String label) {
if (label == null || label.isEmpty()) {
return 0;
}
Integer existing = labelIds.get(label);
if (existing != null) {
return existing;
}
int id = labelIds.size() + 1;
labelIds.put(label, id);
return id;
}
public int labelId(Enum<?> label) {
if (label == null) {
return 0;
}
String key = label.getDeclaringClass().getName() + "#" + label.name();
return labelId(key);
}
public FastPathRouter<ByteView, H> compile() {
return RouteCompiler.compile(routes, paramIds);
}
private void registerParams(RoutePattern pattern) {
for (RoutePattern.Segment segment : pattern.segments()) {
if (segment instanceof RoutePattern.Param) {
paramId(((RoutePattern.Param) segment).name());
} else if (segment instanceof RoutePattern.CatchAll) {
String name = ((RoutePattern.CatchAll) segment).name();
if (name != null && !name.isEmpty()) {
paramId(name);
}
} else if (segment instanceof RoutePattern.Mixed) {
for (String name : ((RoutePattern.Mixed) segment).params()) {
paramId(name);
}
}
}
}
private int paramId(String name) {
Integer existing = paramIds.get(name);
if (existing != null) {
return existing;
}
int id = paramIds.size();
paramIds.put(name, id);
paramNames.add(name);
return id;
}
@Getter
@Accessors(fluent = true)
public static final class RouteSpec<H> {
private final int labelId;
private final RoutePattern pattern;
private final H handler;
private final int order;
public RouteSpec(int labelId, RoutePattern pattern, H handler, int order) {
this.labelId = labelId;
this.pattern = pattern;
this.handler = handler;
this.order = order;
}
}
}
@@ -0,0 +1,130 @@
package dev.relism.fpr.core.dsl;
import dev.relism.fpr.core.RoutePattern;
import java.util.ArrayList;
import java.util.List;
/**
* Cold-path string parser for building RoutePattern instances.
*/
public final class StringRouteParser {
private StringRouteParser() {
}
public static RoutePattern parse(String path) {
if (path == null) {
throw new IllegalArgumentException("path must not be null");
}
String trimmed = path.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("path must not be empty");
}
List<RoutePattern.Segment> segments = new ArrayList<>();
int start = 0;
int len = trimmed.length();
if (trimmed.charAt(0) == '/') {
start = 1;
}
while (true) {
int slash = trimmed.indexOf('/', start);
if (slash == -1) {
slash = len;
}
if (slash > start) {
segments.add(parseSegment(trimmed.substring(start, slash)));
} else if (slash == len) {
break;
} else {
throw new IllegalArgumentException("empty segment in path: " + path);
}
start = slash + 1;
if (start > len) {
break;
}
}
return RoutePattern.fromSegments(segments);
}
private static RoutePattern.Segment parseSegment(String segment) {
if (segment.equals("*")) {
return RoutePattern.wildcard();
}
if (segment.equals("**")) {
return RoutePattern.catchAll();
}
int len = segment.length();
if (len >= 2 && segment.charAt(0) == '{' && segment.charAt(len - 1) == '}'
&& segment.indexOf('{', 1) == -1 && segment.indexOf('}') == len - 1) {
String name = segment.substring(1, len - 1);
if (name.isEmpty()) {
throw new IllegalArgumentException("param segment requires name");
}
if (!isValidParamName(name)) {
throw new IllegalArgumentException("param segment has invalid name: " + segment);
}
return RoutePattern.param(name);
}
if (segment.indexOf('{') >= 0 || segment.indexOf('}') >= 0) {
if (segment.indexOf('{') < 0) {
throw new IllegalArgumentException("mixed segment has stray closing brace: " + segment);
}
return parseMixed(segment);
}
return RoutePattern.literal(segment);
}
private static RoutePattern.Segment parseMixed(String segment) {
List<String> literals = new ArrayList<>();
List<String> params = new ArrayList<>();
int i = 0;
int len = segment.length();
while (i < len) {
int open = segment.indexOf('{', i);
int close = segment.indexOf('}', i);
if (close >= 0 && (open < 0 || close < open)) {
throw new IllegalArgumentException("mixed segment has stray closing brace: " + segment);
}
if (open < 0) {
literals.add(segment.substring(i));
i = len;
break;
}
literals.add(segment.substring(i, open));
int nameStart = open + 1;
int nameEnd = segment.indexOf('}', nameStart);
if (nameEnd < 0) {
throw new IllegalArgumentException("mixed segment has unterminated param: " + segment);
}
if (nameEnd == nameStart) {
throw new IllegalArgumentException("mixed segment has empty param name: " + segment);
}
String name = segment.substring(nameStart, nameEnd);
if (!isValidParamName(name)) {
throw new IllegalArgumentException("mixed segment has invalid param name: " + segment);
}
params.add(name);
i = nameEnd + 1;
}
if (params.isEmpty()) {
return RoutePattern.literal(segment);
}
if (literals.size() == params.size()) {
literals.add("");
}
if (literals.size() != params.size() + 1) {
throw new IllegalArgumentException("mixed segment literals/params mismatch: " + segment);
}
return RoutePattern.mixed(literals.toArray(new String[0]), params.toArray(new String[0]));
}
private static boolean isValidParamName(String name) {
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (!(Character.isLetterOrDigit(ch) || ch == '_')) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,254 @@
package dev.relism.fpr.core.internal.compile;
import dev.relism.fpr.core.ByteView;
import dev.relism.fpr.core.FastPathRouter;
import dev.relism.fpr.core.internal.compile.lookup.LiteralLookupPlanBuilder;
import dev.relism.fpr.core.internal.compile.lookup.MixedLookupPlanBuilder;
import dev.relism.fpr.core.internal.runtime.EdgeKind;
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
import dev.relism.fpr.core.internal.runtime.lookup.LiteralLookupStrategy;
import java.util.Arrays;
import java.util.List;
final class FreezeWriter {
private static final int INDEX_THRESHOLD = 12;
private FreezeWriter() {
}
static <H> FastPathRouter<ByteView, H> freeze(RouteGraph<H> graph) {
List<RouteGraph.Node<H>> nodes = graph.nodes;
List<H> handlers = graph.handlers;
int stateCount = nodes.size();
int totalEdges = 0;
int totalAccepts = 0;
for (RouteGraph.Node<H> node : nodes) {
totalEdges += node.edges.size();
totalAccepts += node.accepts.size();
}
PrimitiveBuilders.ByteBlobBuilder blob = new PrimitiveBuilders.ByteBlobBuilder(1024);
PrimitiveBuilders.IntArrayList edgeNextState = new PrimitiveBuilders.IntArrayList(totalEdges);
PrimitiveBuilders.IntArrayList edgeLabelOff = new PrimitiveBuilders.IntArrayList(totalEdges);
PrimitiveBuilders.ShortArrayList edgeLabelLen = new PrimitiveBuilders.ShortArrayList(totalEdges);
PrimitiveBuilders.LongArrayList edgeLiteralPrefix = new PrimitiveBuilders.LongArrayList(totalEdges);
PrimitiveBuilders.ByteArrayList edgeKind = new PrimitiveBuilders.ByteArrayList(totalEdges);
PrimitiveBuilders.IntArrayList edgeMixedChunkOff = new PrimitiveBuilders.IntArrayList(totalEdges);
PrimitiveBuilders.ShortArrayList edgeMixedChunkCount = new PrimitiveBuilders.ShortArrayList(totalEdges);
PrimitiveBuilders.IntArrayList edgeMixedParamOff = new PrimitiveBuilders.IntArrayList(totalEdges);
PrimitiveBuilders.ShortArrayList edgeMixedParamCount = new PrimitiveBuilders.ShortArrayList(totalEdges);
PrimitiveBuilders.ByteArrayList edgeMixedStrategy = new PrimitiveBuilders.ByteArrayList(totalEdges);
PrimitiveBuilders.IntArrayList mixedChunkOff = new PrimitiveBuilders.IntArrayList(totalEdges * 2);
PrimitiveBuilders.ShortArrayList mixedChunkLen = new PrimitiveBuilders.ShortArrayList(totalEdges * 2);
PrimitiveBuilders.ShortArrayList mixedParamKeyId = new PrimitiveBuilders.ShortArrayList(totalEdges * 2);
PrimitiveBuilders.IntArrayList acceptHandlerId = new PrimitiveBuilders.IntArrayList(totalAccepts);
PrimitiveBuilders.IntArrayList acceptLabelId = new PrimitiveBuilders.IntArrayList(totalAccepts);
PrimitiveBuilders.IntArrayList acceptRouteId = new PrimitiveBuilders.IntArrayList(totalAccepts);
PrimitiveBuilders.IntArrayList indexStart = new PrimitiveBuilders.IntArrayList(stateCount * 4);
PrimitiveBuilders.ShortArrayList indexCount = new PrimitiveBuilders.ShortArrayList(stateCount * 4);
PrimitiveBuilders.IntArrayList indexSecondOff = new PrimitiveBuilders.IntArrayList(stateCount * 4);
PrimitiveBuilders.LongArrayList literalHashKey = new PrimitiveBuilders.LongArrayList(stateCount * 8);
PrimitiveBuilders.IntArrayList literalHashEdge = new PrimitiveBuilders.IntArrayList(stateCount * 8);
int[] stateFirstEdge = new int[stateCount];
short[] stateEdgeCount = new short[stateCount];
int[] stateLiteralStart = new int[stateCount];
short[] stateLiteralCount = new short[stateCount];
byte[] stateLiteralStrategy = new byte[stateCount];
int[] stateLiteralHashOff = new int[stateCount];
int[] stateLiteralHashMask = new int[stateCount];
int[] stateMixedStart = new int[stateCount];
short[] stateMixedCount = new short[stateCount];
short[] stateMixedPrefixCount = new short[stateCount];
int[] stateWildIndex = new int[stateCount];
int[] stateParamNext = new int[stateCount];
short[] stateParamKeyId = new short[stateCount];
int[] stateCatchAllNext = new int[stateCount];
short[] stateCatchAllKeyId = new short[stateCount];
int[] stateAcceptFirst = new int[stateCount];
short[] stateAcceptCount = new short[stateCount];
int[] stateLiteralIndexOff = new int[stateCount];
int[] stateMixedIndexOff = new int[stateCount];
Arrays.fill(stateWildIndex, -1);
Arrays.fill(stateParamNext, -1);
Arrays.fill(stateCatchAllNext, -1);
Arrays.fill(stateParamKeyId, (short) -1);
Arrays.fill(stateCatchAllKeyId, (short) -1);
Arrays.fill(stateLiteralIndexOff, -1);
Arrays.fill(stateMixedIndexOff, -1);
Arrays.fill(stateLiteralHashOff, -1);
Arrays.fill(stateLiteralHashMask, -1);
Arrays.fill(stateLiteralStrategy, LiteralLookupStrategy.LINEAR);
for (int s = 0; s < stateCount; s++) {
RouteGraph.Node<H> node = nodes.get(s);
stateFirstEdge[s] = edgeNextState.size();
List<RouteGraph.Edge> literals = node.literalEdges();
List<RouteGraph.Edge> mixed = node.mixedEdges();
RouteGraph.Edge wild = node.wildEdge();
literals.sort(RouteGraph.Edge.literalComparator());
mixed.sort(RouteGraph.Edge.mixedComparator());
int literalStart = edgeNextState.size();
for (RouteGraph.Edge edge : literals) {
int off = blob.append(edge.literal);
edgeNextState.add(edge.nextState);
edgeLabelOff.add(off);
edgeLabelLen.add((short) edge.literal.length);
edgeLiteralPrefix.add(LiteralLookupPlanBuilder.prefixKey(edge.literal));
edgeKind.add((byte) edge.kind.ordinal());
edgeMixedChunkOff.add(-1);
edgeMixedChunkCount.add((short) 0);
edgeMixedParamOff.add(-1);
edgeMixedParamCount.add((short) 0);
edgeMixedStrategy.add((byte) 0);
}
int literalCount = literals.size();
int mixedStart = edgeNextState.size();
int mixedPrefixCount = 0;
for (RouteGraph.Edge edge : mixed) {
int chunkBase = mixedChunkOff.size();
for (byte[] chunk : edge.mixed.literals) {
int off = blob.append(chunk);
mixedChunkOff.add(off);
mixedChunkLen.add((short) chunk.length);
}
int paramBase = mixedParamKeyId.size();
for (short key : edge.mixed.paramKeys) {
mixedParamKeyId.add(key);
}
int firstOff = mixedChunkOff.get(chunkBase);
short firstLen = mixedChunkLen.get(chunkBase);
edgeNextState.add(edge.nextState);
edgeLabelOff.add(firstOff);
edgeLabelLen.add(firstLen);
edgeLiteralPrefix.add(0L);
edgeKind.add((byte) edge.kind.ordinal());
edgeMixedChunkOff.add(chunkBase);
edgeMixedChunkCount.add((short) edge.mixed.literals.length);
edgeMixedParamOff.add(paramBase);
edgeMixedParamCount.add((short) edge.mixed.paramKeys.length);
edgeMixedStrategy.add(MixedLookupPlanBuilder.strategyForParamCount(edge.mixed.paramKeys.length));
if (firstLen > 0) {
mixedPrefixCount++;
}
}
int mixedCount = mixed.size();
if (wild != null) {
stateWildIndex[s] = edgeNextState.size();
edgeNextState.add(wild.nextState);
edgeLabelOff.add(0);
edgeLabelLen.add((short) 0);
edgeLiteralPrefix.add(0L);
edgeKind.add((byte) EdgeKind.WILD.ordinal());
edgeMixedChunkOff.add(-1);
edgeMixedChunkCount.add((short) 0);
edgeMixedParamOff.add(-1);
edgeMixedParamCount.add((short) 0);
edgeMixedStrategy.add((byte) 0);
}
int edgeCount = edgeNextState.size() - stateFirstEdge[s];
stateEdgeCount[s] = (short) edgeCount;
stateLiteralStart[s] = literalStart;
stateLiteralCount[s] = (short) literalCount;
stateMixedStart[s] = mixedStart;
stateMixedCount[s] = (short) mixedCount;
stateMixedPrefixCount[s] = (short) mixedPrefixCount;
if (literalCount > 0) {
byte literalStrategy = LiteralLookupPlanBuilder.selectStrategy(literalCount);
stateLiteralStrategy[s] = literalStrategy;
if (literalStrategy == LiteralLookupStrategy.HASH) {
LiteralLookupPlanBuilder.HashPlan plan = LiteralLookupPlanBuilder.buildHash(
literalHashKey,
literalHashEdge,
edgeLiteralPrefix,
edgeLabelLen,
literalStart,
literalCount
);
stateLiteralHashOff[s] = plan.offset;
stateLiteralHashMask[s] = plan.mask;
}
}
if (mixedPrefixCount > 0 && mixedCount >= INDEX_THRESHOLD) {
stateMixedIndexOff[s] = IndexBuilder.buildIndex(indexStart, indexCount, indexSecondOff, edgeLabelOff, edgeLabelLen,
blob, mixedStart, mixedCount, false);
}
stateParamNext[s] = node.paramNext;
stateParamKeyId[s] = node.paramKeyId;
stateCatchAllNext[s] = node.catchAllNext;
stateCatchAllKeyId[s] = node.catchAllKeyId;
stateAcceptFirst[s] = acceptHandlerId.size();
for (RouteGraph.Accept accept : node.accepts) {
acceptHandlerId.add(accept.handlerId);
acceptLabelId.add(accept.labelId);
acceptRouteId.add(accept.routeId);
}
stateAcceptCount[s] = (short) node.accepts.size();
}
H[] handlerArray = (H[]) handlers.toArray(new Object[0]);
return new FrozenRouter<>(
blob.toArray(),
handlerArray,
stateFirstEdge,
stateEdgeCount,
stateLiteralStart,
stateLiteralCount,
stateLiteralStrategy,
stateLiteralHashOff,
stateLiteralHashMask,
stateMixedStart,
stateMixedCount,
stateMixedPrefixCount,
stateWildIndex,
stateParamNext,
stateParamKeyId,
stateCatchAllNext,
stateCatchAllKeyId,
stateAcceptFirst,
stateAcceptCount,
stateLiteralIndexOff,
stateMixedIndexOff,
edgeNextState.toArray(),
edgeLabelOff.toArray(),
edgeLabelLen.toArray(),
edgeLiteralPrefix.toArray(),
edgeKind.toArray(),
edgeMixedChunkOff.toArray(),
edgeMixedChunkCount.toArray(),
edgeMixedParamOff.toArray(),
edgeMixedParamCount.toArray(),
edgeMixedStrategy.toArray(),
mixedChunkOff.toArray(),
mixedChunkLen.toArray(),
mixedParamKeyId.toArray(),
acceptHandlerId.toArray(),
acceptLabelId.toArray(),
acceptRouteId.toArray(),
indexStart.toArray(),
indexCount.toArray(),
indexSecondOff.toArray(),
literalHashKey.toArray(),
literalHashEdge.toArray()
);
}
}
@@ -0,0 +1,102 @@
package dev.relism.fpr.core.internal.compile;
import lombok.NoArgsConstructor;
import java.util.Arrays;
@NoArgsConstructor
final class IndexBuilder {
private static final int SECOND_LEVEL_THRESHOLD = 64;
static int buildIndex(PrimitiveBuilders.IntArrayList indexStart,
PrimitiveBuilders.ShortArrayList indexCount,
PrimitiveBuilders.IntArrayList indexSecondOff,
PrimitiveBuilders.IntArrayList edgeLabelOff,
PrimitiveBuilders.ShortArrayList edgeLabelLen,
PrimitiveBuilders.ByteBlobBuilder blob,
int start,
int count,
boolean allowSecondByte) {
int base = allocateTable(indexStart, indexCount, indexSecondOff);
int[] startTmp = new int[256];
short[] countTmp = new short[256];
int[] len2Count = new int[256];
Arrays.fill(startTmp, -1);
int end = start + count;
for (int i = start; i < end; i++) {
int off = edgeLabelOff.get(i);
int len = edgeLabelLen.get(i);
if (len == 0) {
continue;
}
int first = blob.byteAt(off) & 0xFF;
if (startTmp[first] == -1) {
startTmp[first] = i - start;
}
countTmp[first]++;
if (allowSecondByte && len > 1) {
len2Count[first]++;
}
}
for (int i = 0; i < 256; i++) {
indexStart.set(base + i, startTmp[i]);
indexCount.set(base + i, countTmp[i]);
}
if (!allowSecondByte) {
return base;
}
for (int first = 0; first < 256; first++) {
int bucketCount = countTmp[first];
if (bucketCount < SECOND_LEVEL_THRESHOLD || len2Count[first] == 0) {
continue;
}
int relStart = startTmp[first];
if (relStart < 0) {
continue;
}
int bucketStart = start + relStart;
int bucketEnd = bucketStart + bucketCount;
int secondBase = allocateTable(indexStart, indexCount, indexSecondOff);
int[] secondStart = new int[256];
short[] secondCount = new short[256];
Arrays.fill(secondStart, -1);
for (int i = bucketStart; i < bucketEnd; i++) {
int len = edgeLabelLen.get(i);
if (len <= 1) {
continue;
}
int off = edgeLabelOff.get(i);
int second = blob.byteAt(off + 1) & 0xFF;
if (secondStart[second] == -1) {
secondStart[second] = i - start;
}
secondCount[second]++;
}
for (int i = 0; i < 256; i++) {
indexStart.set(secondBase + i, secondStart[i]);
indexCount.set(secondBase + i, secondCount[i]);
}
indexSecondOff.set(base + first, secondBase);
}
return base;
}
private static int allocateTable(PrimitiveBuilders.IntArrayList indexStart,
PrimitiveBuilders.ShortArrayList indexCount,
PrimitiveBuilders.IntArrayList indexSecondOff) {
int base = indexStart.size();
for (int i = 0; i < 256; i++) {
indexStart.add(-1);
indexCount.add((short) 0);
indexSecondOff.add(-1);
}
return base;
}
}
@@ -0,0 +1,204 @@
package dev.relism.fpr.core.internal.compile;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class PrimitiveBuilders {
public static final class IntArrayList {
private int[] data;
private int size;
public IntArrayList(int initial) {
this.data = new int[Math.max(8, initial)];
}
public int size() {
return size;
}
public int get(int index) {
return data[index];
}
public void set(int index, int value) {
data[index] = value;
}
public void add(int value) {
ensure(size + 1);
data[size++] = value;
}
public int[] toArray() {
int[] out = new int[size];
System.arraycopy(data, 0, out, 0, size);
return out;
}
private void ensure(int target) {
if (target <= data.length) {
return;
}
int newCap = Math.max(target, data.length * 2);
int[] next = new int[newCap];
System.arraycopy(data, 0, next, 0, size);
data = next;
}
}
public static final class ShortArrayList {
private short[] data;
private int size;
public ShortArrayList(int initial) {
this.data = new short[Math.max(8, initial)];
}
public int size() {
return size;
}
public short get(int index) {
return data[index];
}
public void set(int index, short value) {
data[index] = value;
}
public void add(short value) {
ensure(size + 1);
data[size++] = value;
}
public short[] toArray() {
short[] out = new short[size];
System.arraycopy(data, 0, out, 0, size);
return out;
}
private void ensure(int target) {
if (target <= data.length) {
return;
}
int newCap = Math.max(target, data.length * 2);
short[] next = new short[newCap];
System.arraycopy(data, 0, next, 0, size);
data = next;
}
}
public static final class ByteArrayList {
private byte[] data;
private int size;
public ByteArrayList(int initial) {
this.data = new byte[Math.max(8, initial)];
}
public int size() {
return size;
}
public void add(byte value) {
ensure(size + 1);
data[size++] = value;
}
public byte[] toArray() {
byte[] out = new byte[size];
System.arraycopy(data, 0, out, 0, size);
return out;
}
private void ensure(int target) {
if (target <= data.length) {
return;
}
int newCap = Math.max(target, data.length * 2);
byte[] next = new byte[newCap];
System.arraycopy(data, 0, next, 0, size);
data = next;
}
}
public static final class LongArrayList {
private long[] data;
private int size;
public LongArrayList(int initial) {
this.data = new long[Math.max(8, initial)];
}
public int size() {
return size;
}
public long get(int index) {
return data[index];
}
public void set(int index, long value) {
data[index] = value;
}
public void add(long value) {
ensure(size + 1);
data[size++] = value;
}
public long[] toArray() {
long[] out = new long[size];
System.arraycopy(data, 0, out, 0, size);
return out;
}
private void ensure(int target) {
if (target <= data.length) {
return;
}
int newCap = Math.max(target, data.length * 2);
long[] next = new long[newCap];
System.arraycopy(data, 0, next, 0, size);
data = next;
}
}
public static final class ByteBlobBuilder {
private byte[] data;
private int size;
public ByteBlobBuilder(int initial) {
this.data = new byte[Math.max(16, initial)];
}
public int append(byte[] bytes) {
int off = size;
ensure(size + bytes.length);
System.arraycopy(bytes, 0, data, size, bytes.length);
size += bytes.length;
return off;
}
public byte byteAt(int index) {
return data[index];
}
public byte[] toArray() {
byte[] out = new byte[size];
System.arraycopy(data, 0, out, 0, size);
return out;
}
private void ensure(int target) {
if (target <= data.length) {
return;
}
int newCap = Math.max(target, data.length * 2);
byte[] next = new byte[newCap];
System.arraycopy(data, 0, next, 0, size);
data = next;
}
}
}
@@ -0,0 +1,70 @@
package dev.relism.fpr.core.internal.compile;
import dev.relism.fpr.core.ByteView;
import dev.relism.fpr.core.FastPathRouter;
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
import dev.relism.fpr.core.RouterBuilder;
import java.util.List;
import java.util.Map;
public final class RouteCompiler {
private RouteCompiler() {
}
public static <H> FastPathRouter<ByteView, H> compile(List<RouterBuilder.RouteSpec<H>> routes,
Map<String, Integer> paramIds) {
if (routes.isEmpty()) {
return emptyRouter();
}
RouteGraph<H> graph = RouteGraph.build(routes, paramIds).canonicalize();
return FreezeWriter.freeze(graph);
}
private static <H> FastPathRouter<ByteView, H> emptyRouter() {
return new FrozenRouter<>(
new byte[0],
(H[]) new Object[0],
new int[]{0}, // stateFirstEdge
new short[]{0}, // stateEdgeCount
new int[]{0}, // stateLiteralStart
new short[]{0}, // stateLiteralCount
new byte[]{0}, // stateLiteralStrategy
new int[]{-1}, // stateLiteralHashOff
new int[]{-1}, // stateLiteralHashMask
new int[]{0}, // stateMixedStart
new short[]{0}, // stateMixedCount
new short[]{0}, // stateMixedPrefixCount
new int[]{-1}, // stateWildIndex
new int[]{-1}, // stateParamNext
new short[]{-1}, // stateParamKeyId
new int[]{-1}, // stateCatchAllNext
new short[]{-1}, // stateCatchAllKeyId
new int[]{0}, // stateAcceptFirst
new short[]{0}, // stateAcceptCount
new int[]{-1}, // stateLiteralIndexOff
new int[]{-1}, // stateMixedIndexOff
new int[0], // edgeNextState
new int[0], // edgeLabelOff
new short[0], // edgeLabelLen
new long[0], // edgeLiteralPrefix
new byte[0], // edgeKind
new int[0], // edgeMixedChunkOff
new short[0], // edgeMixedChunkCount
new int[0], // edgeMixedParamOff
new short[0], // edgeMixedParamCount
new byte[0], // edgeMixedStrategy
new int[0], // mixedChunkOff
new short[0], // mixedChunkLen
new short[0], // mixedParamKeyId
new int[0], // acceptHandlerId
new int[0], // acceptLabelId
new int[0], // acceptRouteId
new int[0], // indexStart
new short[0], // indexCount
new int[0], // indexSecondOff
new long[0], // literalHashKey
new int[0] // literalHashEdge
);
}
}
@@ -0,0 +1,558 @@
package dev.relism.fpr.core.internal.compile;
import dev.relism.fpr.core.RoutePattern;
import dev.relism.fpr.core.RouterBuilder;
import dev.relism.fpr.core.internal.runtime.EdgeKind;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class RouteGraph<H> {
final List<Node<H>> nodes;
final List<H> handlers;
private RouteGraph(List<Node<H>> nodes, List<H> handlers) {
this.nodes = nodes;
this.handlers = handlers;
}
static <H> RouteGraph<H> build(List<RouterBuilder.RouteSpec<H>> routes, Map<String, Integer> paramIds) {
List<RouterBuilder.RouteSpec<H>> ordered = new ArrayList<>(routes);
ordered.sort(routeComparator());
List<Node<H>> nodes = new ArrayList<>();
Node<H> root = new Node<>(0);
nodes.add(root);
List<H> handlers = new ArrayList<>();
int routeId = 0;
for (RouterBuilder.RouteSpec<H> spec : ordered) {
int handlerId = handlers.size();
handlers.add(spec.handler());
Node<H> current = root;
List<RoutePattern.Segment> segments = spec.pattern().segments();
for (int i = 0; i < segments.size(); i++) {
RoutePattern.Segment segment = segments.get(i);
boolean last = i == segments.size() - 1;
switch (segment.type()) {
case LITERAL:
current = current.literal(nodes, literalBytes((RoutePattern.Literal) segment));
break;
case MIXED:
current = current.mixed(nodes, mixedDef((RoutePattern.Mixed) segment, paramIds));
break;
case PARAM:
int paramId = paramId(((RoutePattern.Param) segment).name(), paramIds);
current = current.param(nodes, paramId);
break;
case WILDCARD:
current = current.wild(nodes);
break;
case CATCH_ALL:
if (!last) {
throw new IllegalArgumentException("catch-all must be last segment");
}
Integer catchId = catchId((RoutePattern.CatchAll) segment, paramIds);
current = current.catchAll(nodes, catchId);
break;
default:
throw new IllegalStateException("Unhandled segment type: " + segment.type());
}
}
current.accept(spec.labelId(), handlerId, routeId);
routeId++;
}
return new RouteGraph<>(nodes, handlers);
}
RouteGraph<H> canonicalize() {
int size = nodes.size();
int[] remap = new int[size];
Arrays.fill(remap, -1);
Map<NodeKey, Integer> canonical = new HashMap<>();
List<Node<H>> canonNodes = new ArrayList<>();
for (int i = size - 1; i >= 0; i--) {
Node<H> node = nodes.get(i);
NodeKey key = node.key(remap);
Integer existing = canonical.get(key);
if (existing != null) {
remap[node.id] = existing;
} else {
int id = canonNodes.size();
remap[node.id] = id;
canonical.put(key, id);
canonNodes.add(node);
}
}
for (Node<H> node : canonNodes) {
node.remap(remap);
}
int rootIndex = remap[0];
if (rootIndex != 0) {
int[] reorder = new int[canonNodes.size()];
Arrays.fill(reorder, -1);
reorder[rootIndex] = 0;
int next = 1;
for (int i = 0; i < canonNodes.size(); i++) {
if (i == rootIndex) {
continue;
}
reorder[i] = next++;
}
List<Node<H>> reordered = new ArrayList<>(canonNodes.size());
for (int i = 0; i < canonNodes.size(); i++) {
reordered.add(null);
}
for (int i = 0; i < canonNodes.size(); i++) {
reordered.set(reorder[i], canonNodes.get(i));
}
for (Node<H> node : reordered) {
node.remap(reorder);
}
return new RouteGraph<>(reordered, handlers);
}
return new RouteGraph<>(canonNodes, handlers);
}
private static int paramId(String name, Map<String, Integer> paramIds) {
Integer id = paramIds.get(name);
if (id == null) {
throw new IllegalArgumentException("Unknown param id: " + name);
}
return id;
}
private static Integer catchId(RoutePattern.CatchAll segment, Map<String, Integer> paramIds) {
String name = segment.name();
if (name == null || name.isEmpty()) {
return null;
}
return paramId(name, paramIds);
}
private static byte[] literalBytes(RoutePattern.Literal literal) {
return literal.text().getBytes(StandardCharsets.UTF_8);
}
private static MixedDef mixedDef(RoutePattern.Mixed mixed, Map<String, Integer> paramIds) {
String[] literals = mixed.literals();
String[] params = mixed.params();
byte[][] literalBytes = new byte[literals.length][];
for (int i = 0; i < literals.length; i++) {
literalBytes[i] = literals[i].getBytes(StandardCharsets.UTF_8);
}
short[] keys = new short[params.length];
for (int i = 0; i < params.length; i++) {
keys[i] = (short) paramId(params[i], paramIds);
}
return new MixedDef(literalBytes, keys);
}
private static Comparator<RouterBuilder.RouteSpec<?>> routeComparator() {
return (a, b) -> {
List<RoutePattern.Segment> segA = a.pattern().segments();
List<RoutePattern.Segment> segB = b.pattern().segments();
int min = Math.min(segA.size(), segB.size());
for (int i = 0; i < min; i++) {
int rankA = rank(segA.get(i).type());
int rankB = rank(segB.get(i).type());
if (rankA != rankB) {
return Integer.compare(rankB, rankA);
}
}
if (segA.size() != segB.size()) {
return Integer.compare(segB.size(), segA.size());
}
return Integer.compare(a.order(), b.order());
};
}
private static int rank(RoutePattern.SegmentType type) {
switch (type) {
case LITERAL:
return 5;
case MIXED:
return 4;
case PARAM:
return 3;
case WILDCARD:
return 2;
case CATCH_ALL:
return 1;
default:
return 0;
}
}
static final class MixedDef {
final byte[][] literals;
final short[] paramKeys;
MixedDef(byte[][] literals, short[] paramKeys) {
this.literals = literals;
this.paramKeys = paramKeys;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MixedDef)) {
return false;
}
MixedDef other = (MixedDef) obj;
return Arrays.deepEquals(literals, other.literals) && Arrays.equals(paramKeys, other.paramKeys);
}
@Override
public int hashCode() {
int h = Arrays.deepHashCode(literals);
h = 31 * h + Arrays.hashCode(paramKeys);
return h;
}
}
static final class Accept {
final int labelId;
final int handlerId;
final int routeId;
Accept(int labelId, int handlerId, int routeId) {
this.labelId = labelId;
this.handlerId = handlerId;
this.routeId = routeId;
}
}
static final class Edge {
final EdgeKind kind;
final byte[] literal;
final MixedDef mixed;
int nextState;
Edge(EdgeKind kind, byte[] literal, MixedDef mixed, int nextState) {
this.kind = kind;
this.literal = literal;
this.mixed = mixed;
this.nextState = nextState;
}
static Comparator<Edge> literalComparator() {
return (a, b) -> {
if (a.literal.length != b.literal.length) {
return Integer.compare(a.literal.length, b.literal.length);
}
long aKey = literalPrefix(a.literal);
long bKey = literalPrefix(b.literal);
int cmp = Long.compareUnsigned(aKey, bKey);
if (cmp != 0) {
return cmp;
}
return Arrays.compare(a.literal, b.literal);
};
}
static Comparator<Edge> mixedComparator() {
return (a, b) -> {
int aTotal = totalLiteralLen(a.mixed.literals);
int bTotal = totalLiteralLen(b.mixed.literals);
if (aTotal != bTotal) {
return Integer.compare(bTotal, aTotal);
}
byte[] aFirst = a.mixed.literals[0];
byte[] bFirst = b.mixed.literals[0];
int aLen = aFirst.length;
int bLen = bFirst.length;
if (aLen == 0 && bLen != 0) {
return 1;
}
if (aLen != 0 && bLen == 0) {
return -1;
}
if (aLen != bLen) {
return Integer.compare(bLen, aLen);
}
int cmp = Arrays.compare(aFirst, bFirst);
if (cmp != 0) {
return cmp;
}
int count = Math.min(a.mixed.literals.length, b.mixed.literals.length);
for (int i = 1; i < count; i++) {
cmp = Arrays.compare(a.mixed.literals[i], b.mixed.literals[i]);
if (cmp != 0) {
return cmp;
}
}
return Integer.compare(b.mixed.literals.length, a.mixed.literals.length);
};
}
private static int totalLiteralLen(byte[][] literals) {
int total = 0;
for (byte[] literal : literals) {
total += literal.length;
}
return total;
}
}
private static long literalPrefix(byte[] literal) {
int len = Math.min(8, literal.length);
long key = 0;
for (int i = 0; i < len; i++) {
key |= ((long) literal[i] & 0xFFL) << (i * 8);
}
return key;
}
static final class Node<H> {
final int id;
final List<Edge> edges = new ArrayList<>();
int paramNext = -1;
short paramKeyId = -1;
int catchAllNext = -1;
short catchAllKeyId = -1;
final List<Accept> accepts = new ArrayList<>();
Node(int id) {
this.id = id;
}
Node<H> literal(List<Node<H>> nodes, byte[] literal) {
for (Edge edge : edges) {
if (edge.kind == EdgeKind.LITERAL && Arrays.equals(edge.literal, literal)) {
return nodes.get(edge.nextState);
}
}
Node<H> next = new Node<>(nodes.size());
nodes.add(next);
edges.add(new Edge(EdgeKind.LITERAL, literal, null, next.id));
return next;
}
Node<H> mixed(List<Node<H>> nodes, MixedDef def) {
for (Edge edge : edges) {
if (edge.kind == EdgeKind.MIXED && mixedEquals(edge.mixed, def)) {
if (!Arrays.equals(edge.mixed.paramKeys, def.paramKeys)) {
throw new IllegalArgumentException("Ambiguous mixed segment: conflicting param keys");
}
return nodes.get(edge.nextState);
}
}
Node<H> next = new Node<>(nodes.size());
nodes.add(next);
edges.add(new Edge(EdgeKind.MIXED, null, def, next.id));
return next;
}
Node<H> param(List<Node<H>> nodes, int keyId) {
if (paramNext != -1) {
if (paramKeyId != (short) keyId) {
throw new IllegalArgumentException("Ambiguous param segment at state " + id);
}
return nodes.get(paramNext);
}
Node<H> next = new Node<>(nodes.size());
nodes.add(next);
paramNext = next.id;
paramKeyId = (short) keyId;
return next;
}
Node<H> wild(List<Node<H>> nodes) {
for (Edge edge : edges) {
if (edge.kind == EdgeKind.WILD) {
return nodes.get(edge.nextState);
}
}
Node<H> next = new Node<>(nodes.size());
nodes.add(next);
edges.add(new Edge(EdgeKind.WILD, null, null, next.id));
return next;
}
Node<H> catchAll(List<Node<H>> nodes, Integer keyId) {
if (catchAllNext != -1) {
short existing = catchAllKeyId;
short incoming = keyId == null ? -1 : keyId.shortValue();
if (existing != incoming) {
throw new IllegalArgumentException("Ambiguous catch-all segment at state " + id);
}
return nodes.get(catchAllNext);
}
Node<H> next = new Node<>(nodes.size());
nodes.add(next);
catchAllNext = next.id;
catchAllKeyId = keyId == null ? (short) -1 : keyId.shortValue();
return next;
}
void accept(int labelId, int handlerId, int routeId) {
for (Accept accept : accepts) {
if (accept.labelId == labelId) {
throw new IllegalArgumentException("Ambiguous route: duplicate pattern for label " + labelId);
}
}
accepts.add(new Accept(labelId, handlerId, routeId));
}
List<Edge> literalEdges() {
List<Edge> out = new ArrayList<>();
for (Edge edge : edges) {
if (edge.kind == EdgeKind.LITERAL) {
out.add(edge);
}
}
return out;
}
List<Edge> mixedEdges() {
List<Edge> out = new ArrayList<>();
for (Edge edge : edges) {
if (edge.kind == EdgeKind.MIXED) {
out.add(edge);
}
}
return out;
}
Edge wildEdge() {
for (Edge edge : edges) {
if (edge.kind == EdgeKind.WILD) {
return edge;
}
}
return null;
}
NodeKey key(int[] remap) {
return new NodeKey(this, remap);
}
void remap(int[] remap) {
for (Edge edge : edges) {
edge.nextState = remap[edge.nextState];
}
if (paramNext != -1) {
paramNext = remap[paramNext];
}
if (catchAllNext != -1) {
catchAllNext = remap[catchAllNext];
}
}
}
private static final class NodeKey {
private final int hash;
private final EdgeKind[] edgeKinds;
private final int[] edgeNext;
private final byte[][] edgeLiterals;
private final MixedDef[] edgeMixed;
private final int paramNext;
private final short paramKeyId;
private final int catchNext;
private final short catchKeyId;
private final int[] acceptMethods;
private final int[] acceptHandlers;
private final int[] acceptRoutes;
private NodeKey(Node<?> node, int[] remap) {
this.paramNext = node.paramNext == -1 ? -1 : remap[node.paramNext];
this.paramKeyId = node.paramKeyId;
this.catchNext = node.catchAllNext == -1 ? -1 : remap[node.catchAllNext];
this.catchKeyId = node.catchAllKeyId;
int edgeCount = node.edges.size();
this.edgeKinds = new EdgeKind[edgeCount];
this.edgeNext = new int[edgeCount];
this.edgeLiterals = new byte[edgeCount][];
this.edgeMixed = new MixedDef[edgeCount];
for (int i = 0; i < edgeCount; i++) {
Edge edge = node.edges.get(i);
edgeKinds[i] = edge.kind;
edgeNext[i] = remap[edge.nextState];
edgeLiterals[i] = edge.literal;
edgeMixed[i] = edge.mixed;
}
int acceptCount = node.accepts.size();
acceptMethods = new int[acceptCount];
acceptHandlers = new int[acceptCount];
acceptRoutes = new int[acceptCount];
for (int i = 0; i < acceptCount; i++) {
Accept accept = node.accepts.get(i);
acceptMethods[i] = accept.labelId;
acceptHandlers[i] = accept.handlerId;
acceptRoutes[i] = accept.routeId;
}
this.hash = computeHash();
}
private int computeHash() {
int h = 1;
h = 31 * h + paramNext;
h = 31 * h + paramKeyId;
h = 31 * h + catchNext;
h = 31 * h + catchKeyId;
h = 31 * h + Arrays.hashCode(edgeKinds);
h = 31 * h + Arrays.hashCode(edgeNext);
h = 31 * h + Arrays.deepHashCode(edgeLiterals);
h = 31 * h + Arrays.deepHashCode(edgeMixed);
h = 31 * h + Arrays.hashCode(acceptMethods);
h = 31 * h + Arrays.hashCode(acceptHandlers);
h = 31 * h + Arrays.hashCode(acceptRoutes);
return h;
}
@Override
public int hashCode() {
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof NodeKey)) {
return false;
}
NodeKey other = (NodeKey) obj;
if (paramNext != other.paramNext || paramKeyId != other.paramKeyId) {
return false;
}
if (catchNext != other.catchNext || catchKeyId != other.catchKeyId) {
return false;
}
if (!Arrays.equals(edgeKinds, other.edgeKinds) || !Arrays.equals(edgeNext, other.edgeNext)) {
return false;
}
if (!Arrays.deepEquals(edgeLiterals, other.edgeLiterals)) {
return false;
}
if (!Arrays.deepEquals(edgeMixed, other.edgeMixed)) {
return false;
}
return Arrays.equals(acceptMethods, other.acceptMethods)
&& Arrays.equals(acceptHandlers, other.acceptHandlers)
&& Arrays.equals(acceptRoutes, other.acceptRoutes);
}
}
private static boolean mixedEquals(MixedDef a, MixedDef b) {
return Arrays.deepEquals(a.literals, b.literals);
}
}
@@ -0,0 +1,86 @@
package dev.relism.fpr.core.internal.compile.lookup;
import dev.relism.fpr.core.internal.compile.PrimitiveBuilders;
import dev.relism.fpr.core.internal.runtime.lookup.LiteralLookupStrategy;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class LiteralLookupPlanBuilder {
private static final int LINEAR_LIMIT = 16;
private static final int ORDERED_LIMIT = 256;
public static byte selectStrategy(int literalCount) {
if (literalCount <= LINEAR_LIMIT) {
return LiteralLookupStrategy.LINEAR;
}
if (literalCount <= ORDERED_LIMIT) {
return LiteralLookupStrategy.ORDERED_PREFIX;
}
return LiteralLookupStrategy.HASH;
}
public static long prefixKey(byte[] literal) {
int len = Math.min(8, literal.length);
long key = 0;
for (int i = 0; i < len; i++) {
key |= ((long) literal[i] & 0xFFL) << (i * 8);
}
return key;
}
public static HashPlan buildHash(PrimitiveBuilders.LongArrayList hashKeys,
PrimitiveBuilders.IntArrayList hashEdges,
PrimitiveBuilders.LongArrayList edgePrefix,
PrimitiveBuilders.ShortArrayList edgeLabelLen,
int start,
int count) {
int size = tableSize(count);
int mask = size - 1;
int off = hashKeys.size();
for (int i = 0; i < size; i++) {
hashKeys.add(0L);
hashEdges.add(-1);
}
int end = start + count;
for (int i = start; i < end; i++) {
int len = edgeLabelLen.get(i);
if (len <= 0) {
continue;
}
long key = edgePrefix.get(i);
int slot = mix(key) & mask;
while (hashKeys.get(off + slot) != 0L) {
slot = (slot + 1) & mask;
}
hashKeys.set(off + slot, key);
hashEdges.set(off + slot, i);
}
return new HashPlan(off, mask);
}
private static int tableSize(int count) {
int size = 1;
int target = Math.max(4, count * 2);
while (size < target) {
size <<= 1;
}
return size;
}
private static int mix(long key) {
long h = key ^ (key >>> 33);
h *= 0xff51afd7ed558ccdL;
h ^= (h >>> 33);
h *= 0xc4ceb9fe1a85ec53L;
h ^= (h >>> 33);
return (int) h;
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class HashPlan {
public final int offset;
public final int mask;
}
}
@@ -0,0 +1,18 @@
package dev.relism.fpr.core.internal.compile.lookup;
import dev.relism.fpr.core.internal.runtime.lookup.MixedLookupStrategy;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MixedLookupPlanBuilder {
public static byte strategyForParamCount(int paramCount) {
if (paramCount <= 1) {
return MixedLookupStrategy.ONE;
}
if (paramCount == 2) {
return MixedLookupStrategy.TWO;
}
return MixedLookupStrategy.N;
}
}
@@ -0,0 +1,53 @@
package dev.relism.fpr.core.internal.runtime;
import dev.relism.fpr.core.ByteView;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ByteCompare {
private static final VarHandle LONG_VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN);
public static boolean equals(ByteView view, int start, byte[] blob, int off, int len, boolean supportsLong) {
if (len == 0) {
return true;
}
int i = 0;
if (supportsLong && len >= 8) {
int end = len - 8;
for (; i <= end; i += 8) {
long a = view.longAt(start + i);
long b = (long) LONG_VIEW.get(blob, off + i);
if (a != b) {
return false;
}
}
}
for (; i < len; i++) {
if (view.byteAt(start + i) != blob[off + i]) {
return false;
}
}
return true;
}
public static int indexOf(ByteView view, int start, int max, byte[] blob, int off, int len, boolean supportsLong) {
if (len == 0) {
return start;
}
byte first = blob[off];
for (int i = start; i <= max; i++) {
if (view.byteAt(i) != first) {
continue;
}
if (equals(view, i, blob, off, len, supportsLong)) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,35 @@
package dev.relism.fpr.core.internal.runtime;
final class EdgeDispatch {
private EdgeDispatch() {
}
static long mixedPrefixRange(FrozenRouter<?> router, int state, int firstByte) {
int prefixCount = router.stateMixedPrefixCount[state];
if (prefixCount <= 0) {
return range(0, 0);
}
int indexOff = router.stateMixedIndexOff[state];
if (indexOff >= 0) {
int rangeStartRel = router.indexStart[indexOff + firstByte];
int rangeCount = router.indexCount[indexOff + firstByte];
if (rangeCount == 0 || rangeStartRel < 0) {
return range(0, 0);
}
return range(router.stateMixedStart[state] + rangeStartRel, rangeCount);
}
return range(router.stateMixedStart[state], router.stateMixedCount[state]);
}
static long range(int start, int count) {
return ((long) start << 32) | (count & 0xffffffffL);
}
static int rangeStart(long range) {
return (int) (range >>> 32);
}
static int rangeCount(long range) {
return (int) range;
}
}
@@ -0,0 +1,9 @@
package dev.relism.fpr.core.internal.runtime;
public enum EdgeKind {
LITERAL,
MIXED,
PARAM,
WILD,
CATCH
}
@@ -0,0 +1,250 @@
package dev.relism.fpr.core.internal.runtime;
import dev.relism.fpr.core.ByteView;
import dev.relism.fpr.core.FastPathRouter;
import dev.relism.fpr.core.MatchResult;
import dev.relism.fpr.core.internal.runtime.lookup.MixedLookup;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public final class FrozenRouter<H> implements FastPathRouter<ByteView, H> {
public final byte[] blob;
public final H[] handlers;
public final int[] stateFirstEdge;
public final short[] stateEdgeCount;
public final int[] stateLiteralStart;
public final short[] stateLiteralCount;
public final byte[] stateLiteralStrategy;
public final int[] stateLiteralHashOff;
public final int[] stateLiteralHashMask;
public final int[] stateMixedStart;
public final short[] stateMixedCount;
public final short[] stateMixedPrefixCount;
public final int[] stateWildIndex;
public final int[] stateParamNext;
public final short[] stateParamKeyId;
public final int[] stateCatchAllNext;
public final short[] stateCatchAllKeyId;
public final int[] stateAcceptFirst;
public final short[] stateAcceptCount;
public final int[] stateLiteralIndexOff;
public final int[] stateMixedIndexOff;
public final int[] edgeNextState;
public final int[] edgeLabelOff;
public final short[] edgeLabelLen;
public final long[] edgeLiteralPrefix;
public final byte[] edgeKind;
public final int[] edgeMixedChunkOff;
public final short[] edgeMixedChunkCount;
public final int[] edgeMixedParamOff;
public final short[] edgeMixedParamCount;
public final byte[] edgeMixedStrategy;
public final int[] mixedChunkOff;
public final short[] mixedChunkLen;
public final short[] mixedParamKeyId;
public final int[] acceptHandlerId;
public final int[] acceptLabelId;
public final int[] acceptRouteId;
public final int[] indexStart;
public final short[] indexCount;
public final int[] indexSecondOff;
public final long[] literalHashKey;
public final int[] literalHashEdge;
/**
* Matches without allocations, using the provided reusable MatchResult buffer.
*/
@Override
public int match(ByteView input, MatchResult<H> out) {
RouteSearch search = new RouteSearch();
SegmentCursor cursor = new SegmentCursor();
out.reset();
boolean supportsLong = input.supportsLong();
cursor.reset(input);
search.reset(out);
RouteSearch.Frame frame = search.frame();
int len = cursor.length();
int state = 0;
boolean hasSegment = cursor.advance();
final byte kindLiteral = (byte) EdgeKind.LITERAL.ordinal();
final byte kindMixed = (byte) EdgeKind.MIXED.ordinal();
final byte kindParam = (byte) EdgeKind.PARAM.ordinal();
final byte kindWild = (byte) EdgeKind.WILD.ordinal();
final byte kindCatch = (byte) EdgeKind.CATCH.ordinal();
while (true) {
if (!hasSegment) {
int accept = accept(state, out);
if (accept != NO_MATCH) {
return accept;
}
if (stateCatchAllNext[state] != -1) {
short key = stateCatchAllKeyId[state];
if (key >= 0) {
out.addParam(key, len, 0);
}
return accept(stateCatchAllNext[state], out);
}
long backtracked = backtrack(frame, input, supportsLong, out,
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
if (backtracked == BACKTRACK_NO_MATCH) {
return NO_MATCH;
}
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
return (int) backtracked;
}
state = (int) backtracked;
hasSegment = cursor.advance();
continue;
}
int segStart = cursor.segStart();
int segLen = cursor.segLen();
if (segLen <= 0) {
long backtracked = backtrack(frame, input, supportsLong, out,
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
if (backtracked == BACKTRACK_NO_MATCH) {
return NO_MATCH;
}
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
return (int) backtracked;
}
state = (int) backtracked;
hasSegment = cursor.advance();
continue;
}
long candidate = search.selectCandidate(this, state, input, cursor, supportsLong, out);
if (candidate == RouteSearch.NO_CANDIDATE) {
long backtracked = backtrack(frame, input, supportsLong, out,
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
if (backtracked == BACKTRACK_NO_MATCH) {
return NO_MATCH;
}
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
return (int) backtracked;
}
state = (int) backtracked;
hasSegment = cursor.advance();
continue;
}
byte kind = RouteSearch.kind(candidate);
int edgeIndex = RouteSearch.edgeIndex(candidate);
if (kind == kindLiteral) {
state = edgeNextState[edgeIndex];
} else if (kind == kindMixed) {
state = edgeNextState[edgeIndex];
} else if (kind == kindParam) {
out.addParam(stateParamKeyId[state], segStart, segLen);
state = stateParamNext[state];
} else if (kind == kindWild) {
state = edgeNextState[edgeIndex];
} else if (kind == kindCatch) {
short key = stateCatchAllKeyId[state];
if (key >= 0) {
out.addParam(key, segStart, len - segStart);
}
return accept(stateCatchAllNext[state], out);
} else {
state = NO_MATCH;
}
if (state == NO_MATCH) {
long backtracked = backtrack(frame, input, supportsLong, out,
kindLiteral, kindMixed, kindParam, kindWild, kindCatch, len, search, cursor);
if (backtracked == BACKTRACK_NO_MATCH) {
return NO_MATCH;
}
if ((backtracked & BACKTRACK_ACCEPT_MASK) != 0) {
return (int) backtracked;
}
state = (int) backtracked;
hasSegment = cursor.advance();
continue;
}
hasSegment = cursor.advance();
}
}
private long backtrack(RouteSearch.Frame frame,
ByteView input,
boolean supportsLong,
MatchResult<H> out,
byte kindLiteral,
byte kindMixed,
byte kindParam,
byte kindWild,
byte kindCatch,
int len,
RouteSearch search,
SegmentCursor cursor) {
while (search.popInto(frame)) {
out.rollbackTo(frame.paramMark);
cursor.restore(frame.segStart, frame.segLen, frame.nextIdx);
if (frame.kind == kindCatch) {
int catchNext = stateCatchAllNext[frame.state];
if (catchNext == -1) {
continue;
}
short key = stateCatchAllKeyId[frame.state];
if (key >= 0) {
out.addParam(key, frame.segStart, len - frame.segStart);
}
return BACKTRACK_ACCEPT_MASK | (accept(catchNext, out) & 0xffffffffL);
}
int next;
if (frame.kind == kindLiteral) {
next = edgeNextState[frame.edgeIndex];
} else if (frame.kind == kindMixed) {
if (!MixedLookup.match(this, frame.edgeIndex, input, frame.segStart, frame.segLen, supportsLong, out)) {
continue;
}
next = edgeNextState[frame.edgeIndex];
} else if (frame.kind == kindParam) {
out.addParam(stateParamKeyId[frame.state], frame.segStart, frame.segLen);
next = stateParamNext[frame.state];
} else if (frame.kind == kindWild) {
next = edgeNextState[frame.edgeIndex];
} else {
continue;
}
if (next >= 0) {
return next;
}
}
return BACKTRACK_NO_MATCH;
}
int accept(int state, MatchResult<H> out) {
int labelId = out.labelId();
int start = stateAcceptFirst[state];
int count = stateAcceptCount[state];
for (int i = 0; i < count; i++) {
int idx = start + i;
int acceptLabel = acceptLabelId[idx];
if (acceptLabel == 0 || acceptLabel == labelId) {
out.setHandler(handlers[acceptHandlerId[idx]]);
return acceptRouteId[idx];
}
}
return NO_MATCH;
}
private static final long BACKTRACK_ACCEPT_MASK = 0x4000000000000000L;
private static final long BACKTRACK_NO_MATCH = -1L;
private static final int NO_MATCH = FastPathRouter.NO_MATCH;
}
@@ -0,0 +1,245 @@
package dev.relism.fpr.core.internal.runtime;
import dev.relism.fpr.core.ByteView;
import dev.relism.fpr.core.MatchResult;
import dev.relism.fpr.core.MatchResultAccess;
import dev.relism.fpr.core.internal.runtime.lookup.LiteralLookup;
import dev.relism.fpr.core.internal.runtime.lookup.MixedLookup;
final class RouteSearch {
static final long NO_CANDIDATE = -1L;
private int[] stackState;
private int[] stackSegStart;
private int[] stackSegLen;
private int[] stackNextIdx;
private int[] stackParamMark;
private int[] stackEdgeIndex;
private byte[] stackKind;
private int[] keyIds;
private int[] starts;
private int[] lens;
private int[] scratchKeyIds;
private int[] scratchStarts;
private int[] scratchLens;
private int[] scratchEdges;
private int stackSize;
private final Frame frame = new Frame();
private final byte kindLiteral = (byte) EdgeKind.LITERAL.ordinal();
private final byte kindMixed = (byte) EdgeKind.MIXED.ordinal();
private final byte kindParam = (byte) EdgeKind.PARAM.ordinal();
private final byte kindWild = (byte) EdgeKind.WILD.ordinal();
private final byte kindCatch = (byte) EdgeKind.CATCH.ordinal();
void reset(MatchResult<?> out) {
this.stackState = MatchResultAccess.stackState(out);
this.stackSegStart = MatchResultAccess.stackSegStart(out);
this.stackSegLen = MatchResultAccess.stackSegLen(out);
this.stackNextIdx = MatchResultAccess.stackNextIdx(out);
this.stackParamMark = MatchResultAccess.stackParamMark(out);
this.stackEdgeIndex = MatchResultAccess.stackEdgeIndex(out);
this.stackKind = MatchResultAccess.stackKind(out);
this.keyIds = MatchResultAccess.keyIds(out);
this.starts = MatchResultAccess.starts(out);
this.lens = MatchResultAccess.lens(out);
this.scratchKeyIds = MatchResultAccess.scratchKeyIds(out);
this.scratchStarts = MatchResultAccess.scratchStarts(out);
this.scratchLens = MatchResultAccess.scratchLens(out);
this.scratchEdges = MatchResultAccess.scratchEdges(out);
this.stackSize = 0;
}
Frame frame() {
return frame;
}
long selectCandidate(FrozenRouter<?> router,
int state,
ByteView input,
SegmentCursor cursor,
boolean supportsLong,
MatchResult<?> out) {
int segStart = cursor.segStart();
int segLen = cursor.segLen();
int nextIdx = cursor.nextIdx();
int mark = out.mark();
int literalEdge = LiteralLookup.find(router, input, segStart, segLen, supportsLong, state);
boolean keepMixedParams = literalEdge == -1;
int mixedCount = router.stateMixedCount[state];
int mixedStart = router.stateMixedStart[state];
int mixedEnd = mixedStart + mixedCount;
int mixedMatchCount = 0;
int mixedParamCount = 0;
if (mixedCount > 0) {
int firstByte = input.byteAt(segStart) & 0xFF;
long prefixRange = EdgeDispatch.mixedPrefixRange(router, state, firstByte);
int rangeStart = EdgeDispatch.rangeStart(prefixRange);
int rangeCount = EdgeDispatch.rangeCount(prefixRange);
if (rangeCount > 0) {
int end = rangeStart + rangeCount;
for (int i = rangeStart; i < end; i++) {
if (router.edgeLabelLen[i] == 0) {
continue;
}
if (MixedLookup.match(router, i, input, segStart, segLen, supportsLong, out)) {
if (mixedMatchCount >= scratchEdges.length) {
throw new IllegalStateException("MatchResult stack exhausted");
}
scratchEdges[mixedMatchCount++] = i;
if (mixedMatchCount == 1 && keepMixedParams) {
mixedParamCount = out.paramCount() - mark;
if (mixedParamCount > 0) {
System.arraycopy(keyIds, mark, scratchKeyIds, 0, mixedParamCount);
System.arraycopy(starts, mark, scratchStarts, 0, mixedParamCount);
System.arraycopy(lens, mark, scratchLens, 0, mixedParamCount);
}
}
out.rollbackTo(mark);
}
}
}
if (mixedCount > router.stateMixedPrefixCount[state]) {
for (int i = mixedStart; i < mixedEnd; i++) {
if (router.edgeLabelLen[i] != 0) {
continue;
}
if (MixedLookup.match(router, i, input, segStart, segLen, supportsLong, out)) {
if (mixedMatchCount >= scratchEdges.length) {
throw new IllegalStateException("MatchResult stack exhausted");
}
scratchEdges[mixedMatchCount++] = i;
if (mixedMatchCount == 1 && keepMixedParams) {
mixedParamCount = out.paramCount() - mark;
if (mixedParamCount > 0) {
System.arraycopy(keyIds, mark, scratchKeyIds, 0, mixedParamCount);
System.arraycopy(starts, mark, scratchStarts, 0, mixedParamCount);
System.arraycopy(lens, mark, scratchLens, 0, mixedParamCount);
}
}
out.rollbackTo(mark);
}
}
}
}
int mixedFirst = mixedMatchCount > 0 ? scratchEdges[0] : -1;
int paramNext = router.stateParamNext[state];
int wildIndex = router.stateWildIndex[state];
int catchNext = router.stateCatchAllNext[state];
byte kind = -1;
int edgeIndex = -1;
if (literalEdge != -1) {
kind = kindLiteral;
edgeIndex = literalEdge;
} else if (mixedFirst != -1) {
kind = kindMixed;
edgeIndex = mixedFirst;
} else if (paramNext != -1) {
kind = kindParam;
} else if (wildIndex != -1) {
kind = kindWild;
edgeIndex = wildIndex;
} else if (catchNext != -1) {
kind = kindCatch;
}
if (kind == -1) {
return NO_CANDIDATE;
}
if (catchNext != -1 && kind != kindCatch) {
push(kindCatch, state, -1, segStart, segLen, nextIdx, mark);
}
if (wildIndex != -1 && kind != kindWild) {
push(kindWild, state, wildIndex, segStart, segLen, nextIdx, mark);
}
if (paramNext != -1 && kind != kindParam) {
push(kindParam, state, -1, segStart, segLen, nextIdx, mark);
}
if (mixedMatchCount > 0 && (kind != kindMixed || mixedMatchCount > 1)) {
for (int i = mixedMatchCount - 1; i >= 0; i--) {
if (kind == kindMixed && i == 0) {
continue;
}
push(kindMixed, state, scratchEdges[i], segStart, segLen, nextIdx, mark);
}
}
if (kind == kindMixed) {
out.rollbackTo(mark);
if (keepMixedParams) {
if (mixedParamCount > 0) {
System.arraycopy(scratchKeyIds, 0, keyIds, mark, mixedParamCount);
System.arraycopy(scratchStarts, 0, starts, mark, mixedParamCount);
System.arraycopy(scratchLens, 0, lens, mark, mixedParamCount);
}
MatchResultAccess.paramCount(out, mark + mixedParamCount);
}
} else {
out.rollbackTo(mark);
}
return pack(kind, edgeIndex);
}
boolean popInto(Frame out) {
if (stackSize == 0) {
return false;
}
int idx = --stackSize;
out.state = stackState[idx];
out.segStart = stackSegStart[idx];
out.segLen = stackSegLen[idx];
out.nextIdx = stackNextIdx[idx];
out.paramMark = stackParamMark[idx];
out.edgeIndex = stackEdgeIndex[idx];
out.kind = stackKind[idx];
return true;
}
private void push(byte kind,
int state,
int edgeIndex,
int segStart,
int segLen,
int nextIdx,
int paramMark) {
if (stackSize >= stackState.length) {
throw new IllegalStateException("MatchResult stack exhausted");
}
stackState[stackSize] = state;
stackSegStart[stackSize] = segStart;
stackSegLen[stackSize] = segLen;
stackNextIdx[stackSize] = nextIdx;
stackParamMark[stackSize] = paramMark;
stackEdgeIndex[stackSize] = edgeIndex;
stackKind[stackSize] = kind;
stackSize++;
}
private static long pack(byte kind, int edgeIndex) {
return ((long) kind << 32) | (edgeIndex & 0xffffffffL);
}
static byte kind(long packed) {
return (byte) (packed >>> 32);
}
static int edgeIndex(long packed) {
return (int) packed;
}
static final class Frame {
int state;
int segStart;
int segLen;
int nextIdx;
int paramMark;
int edgeIndex;
byte kind;
}
}
@@ -0,0 +1,64 @@
package dev.relism.fpr.core.internal.runtime;
import dev.relism.fpr.core.ByteView;
final class SegmentCursor {
private ByteView input;
private int len;
private int idx;
private int segStart;
private int segLen;
private int nextIdx;
void reset(ByteView input) {
this.input = input;
this.len = input.length();
this.idx = 0;
if (idx < len && input.byteAt(idx) == '/') {
idx++;
}
this.segStart = 0;
this.segLen = -1;
this.nextIdx = idx;
}
boolean advance() {
if (idx >= len) {
return false;
}
segStart = idx;
while (idx < len && input.byteAt(idx) != '/') {
idx++;
}
segLen = idx - segStart;
nextIdx = idx;
if (idx < len && input.byteAt(idx) == '/') {
nextIdx = idx + 1;
}
idx = nextIdx;
return true;
}
void restore(int segStart, int segLen, int nextIdx) {
this.segStart = segStart;
this.segLen = segLen;
this.nextIdx = nextIdx;
this.idx = nextIdx;
}
int segStart() {
return segStart;
}
int segLen() {
return segLen;
}
int nextIdx() {
return nextIdx;
}
int length() {
return len;
}
}
@@ -0,0 +1,160 @@
package dev.relism.fpr.core.internal.runtime.lookup;
import dev.relism.fpr.core.ByteView;
import dev.relism.fpr.core.internal.runtime.ByteCompare;
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Lookup strategies for literal edges, selected per state at compile-time.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class LiteralLookup {
public static int find(FrozenRouter<?> router,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
int state) {
int count = router.stateLiteralCount[state];
if (count == 0) {
return -1;
}
int start = router.stateLiteralStart[state];
byte strategy = router.stateLiteralStrategy[state];
if (strategy == LiteralLookupStrategy.LINEAR) {
return linear(router, input, segStart, segLen, supportsLong, start, count);
}
if (strategy == LiteralLookupStrategy.ORDERED_PREFIX) {
return orderedPrefix(router, input, segStart, segLen, supportsLong, start, count);
}
if (strategy == LiteralLookupStrategy.HASH) {
return hash(router, input, segStart, segLen, supportsLong, state, start, count);
}
return linear(router, input, segStart, segLen, supportsLong, start, count);
}
private static int linear(FrozenRouter<?> router,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
int start,
int count) {
int end = start + count;
for (int i = start; i < end; i++) {
if (router.edgeLabelLen[i] != segLen) {
continue;
}
if (ByteCompare.equals(input, segStart, router.blob, router.edgeLabelOff[i], segLen, supportsLong)) {
return i;
}
}
return -1;
}
private static int orderedPrefix(FrozenRouter<?> router,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
int start,
int count) {
long key = prefixKey(input, segStart, segLen, supportsLong);
int lo = start;
int hi = start + count - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
long midKey = router.edgeLiteralPrefix[mid];
int cmp = compare(segLen, key, router.edgeLabelLen[mid], midKey);
if (cmp < 0) {
hi = mid - 1;
} else if (cmp > 0) {
lo = mid + 1;
} else {
int left = mid;
while (left > start && compare(segLen, key, router.edgeLabelLen[left - 1], router.edgeLiteralPrefix[left - 1]) == 0) {
left--;
}
int right = mid;
int end = start + count;
while (right + 1 < end && compare(segLen, key, router.edgeLabelLen[right + 1], router.edgeLiteralPrefix[right + 1]) == 0) {
right++;
}
for (int i = left; i <= right; i++) {
if (router.edgeLabelLen[i] != segLen) {
continue;
}
if (ByteCompare.equals(input, segStart, router.blob, router.edgeLabelOff[i], segLen, supportsLong)) {
return i;
}
}
return -1;
}
}
return -1;
}
private static int hash(FrozenRouter<?> router,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
int state,
int start,
int count) {
int mask = router.stateLiteralHashMask[state];
int off = router.stateLiteralHashOff[state];
if (mask <= 0 || off < 0) {
return linear(router, input, segStart, segLen, supportsLong, start, count);
}
long key = prefixKey(input, segStart, segLen, supportsLong);
int slot = mix(key) & mask;
while (true) {
long stored = router.literalHashKey[off + slot];
if (stored == 0L) {
return -1;
}
if (stored == key) {
int edgeIndex = router.literalHashEdge[off + slot];
if (edgeIndex >= start && edgeIndex < start + count && router.edgeLabelLen[edgeIndex] == segLen
&& ByteCompare.equals(input, segStart, router.blob, router.edgeLabelOff[edgeIndex], segLen, supportsLong)) {
return edgeIndex;
}
}
slot = (slot + 1) & mask;
}
}
private static int compare(int aLen, long aKey, short bLen, long bKey) {
if (aLen != bLen) {
return Integer.compare(aLen, bLen);
}
return Long.compareUnsigned(aKey, bKey);
}
private static long prefixKey(ByteView input,
int segStart,
int segLen,
boolean supportsLong) {
if (segLen >= 8 && supportsLong && segStart + 8 <= input.length()) {
return input.longAt(segStart);
}
long key = 0;
int len = Math.min(8, segLen);
for (int i = 0; i < len; i++) {
key |= ((long) input.byteAt(segStart + i) & 0xFFL) << (i * 8);
}
return key;
}
private static int mix(long key) {
long h = key ^ (key >>> 33);
h *= 0xff51afd7ed558ccdL;
h ^= (h >>> 33);
h *= 0xc4ceb9fe1a85ec53L;
h ^= (h >>> 33);
return (int) h;
}
}
@@ -0,0 +1,14 @@
package dev.relism.fpr.core.internal.runtime.lookup;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Strategy ids for literal edge lookup, selected at compile time.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class LiteralLookupStrategy {
public static final byte LINEAR = 0;
public static final byte ORDERED_PREFIX = 1;
public static final byte HASH = 2;
}
@@ -0,0 +1,102 @@
package dev.relism.fpr.core.internal.runtime.lookup;
import dev.relism.fpr.core.ByteView;
import dev.relism.fpr.core.MatchResult;
import dev.relism.fpr.core.internal.runtime.ByteCompare;
import dev.relism.fpr.core.internal.runtime.FrozenRouter;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Matching strategies for mixed literal/param segments.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MixedLookup {
public static boolean match(FrozenRouter<?> router,
int edgeIndex,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
MatchResult<?> out) {
byte strategy = router.edgeMixedStrategy[edgeIndex];
if (strategy == MixedLookupStrategy.ONE) {
return matchOne(router, edgeIndex, input, segStart, segLen, supportsLong, out);
}
return matchGeneral(router, edgeIndex, input, segStart, segLen, supportsLong, out);
}
private static boolean matchOne(FrozenRouter<?> router,
int edgeIndex,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
MatchResult<?> out) {
int chunkOff = router.edgeMixedChunkOff[edgeIndex];
int paramOff = router.edgeMixedParamOff[edgeIndex];
int lit0Off = router.mixedChunkOff[chunkOff];
int lit0Len = router.mixedChunkLen[chunkOff];
int lit1Off = router.mixedChunkOff[chunkOff + 1];
int lit1Len = router.mixedChunkLen[chunkOff + 1];
int required = lit0Len + lit1Len;
int paramLen = segLen - required;
if (paramLen <= 0) {
return false;
}
if (!ByteCompare.equals(input, segStart, router.blob, lit0Off, lit0Len, supportsLong)) {
return false;
}
int suffixStart = segStart + segLen - lit1Len;
if (!ByteCompare.equals(input, suffixStart, router.blob, lit1Off, lit1Len, supportsLong)) {
return false;
}
int paramStart = segStart + lit0Len;
out.addParam(router.mixedParamKeyId[paramOff], paramStart, paramLen);
return true;
}
private static boolean matchGeneral(FrozenRouter<?> router,
int edgeIndex,
ByteView input,
int segStart,
int segLen,
boolean supportsLong,
MatchResult<?> out) {
int chunkOff = router.edgeMixedChunkOff[edgeIndex];
int chunkCount = router.edgeMixedChunkCount[edgeIndex];
int paramOff = router.edgeMixedParamOff[edgeIndex];
int paramCount = router.edgeMixedParamCount[edgeIndex];
int end = segStart + segLen;
int cursor = segStart;
for (int i = 0; i < paramCount; i++) {
int litOff = router.mixedChunkOff[chunkOff + i];
int litLen = router.mixedChunkLen[chunkOff + i];
if (!ByteCompare.equals(input, cursor, router.blob, litOff, litLen, supportsLong)) {
return false;
}
cursor += litLen;
int nextLitOff = router.mixedChunkOff[chunkOff + i + 1];
int nextLitLen = router.mixedChunkLen[chunkOff + i + 1];
int nextPos;
if (nextLitLen == 0) {
nextPos = end;
} else {
nextPos = ByteCompare.indexOf(input, cursor, end - nextLitLen, router.blob, nextLitOff, nextLitLen, supportsLong);
if (nextPos < 0) {
return false;
}
}
int paramLen = nextPos - cursor;
if (paramLen <= 0) {
return false;
}
out.addParam(router.mixedParamKeyId[paramOff + i], cursor, paramLen);
cursor = nextPos;
}
int tailOff = router.mixedChunkOff[chunkOff + chunkCount - 1];
int tailLen = router.mixedChunkLen[chunkOff + chunkCount - 1];
return ByteCompare.equals(input, cursor, router.blob, tailOff, tailLen, supportsLong);
}
}
@@ -0,0 +1,14 @@
package dev.relism.fpr.core.internal.runtime.lookup;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Strategy ids for mixed-segment matching, selected at compile time.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MixedLookupStrategy {
public static final byte ONE = 0;
public static final byte TWO = 1;
public static final byte N = 2;
}
@@ -0,0 +1,145 @@
package dev.relism.fpr.core;
import dev.relism.fpr.core.dsl.StringRouteParser;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RouterConcurrencyTest {
@Test
void testConcurrentMatchingDoesNotCorruptState() throws InterruptedException {
int threadCount = 20;
int iterationsPerThread = 100_000;
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/users"), "USERS_LIST");
builder.add(StringRouteParser.parse("/users/{id}"), "USER_DETAIL");
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "USER_ORDER");
builder.add(StringRouteParser.parse("/static/pre-{x}-suf"), "STATIC_MIXED");
builder.add(StringRouteParser.parse("/assets/**"), "ASSETS_CATCH_ALL");
FastPathRouter<ByteView, String> router = builder.compile();
byte[] path1 = "/users".getBytes(StandardCharsets.US_ASCII);
byte[] path2 = "/users/123".getBytes(StandardCharsets.US_ASCII);
byte[] path3 = "/users/123/orders/abc".getBytes(StandardCharsets.US_ASCII);
byte[] path4 = "/static/pre-xyz-suf".getBytes(StandardCharsets.US_ASCII);
byte[] path5 = "/assets/css/style.css".getBytes(StandardCharsets.US_ASCII);
byte[] path6 = "/not-found".getBytes(StandardCharsets.US_ASCII);
ByteView[] views = new ByteView[] {
new ByteArrayView(path1),
new ByteArrayView(path2),
new ByteArrayView(path3),
new ByteArrayView(path4),
new ByteArrayView(path5),
new ByteArrayView(path6)
};
String[] expectedHandlers = new String[] {
"USERS_LIST",
"USER_DETAIL",
"USER_ORDER",
"STATIC_MIXED",
"ASSETS_CATCH_ALL",
null
};
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
final int threadIndex = i;
futures.add(executor.submit((Callable<Integer>) () -> {
MatchResult<String> result = new MatchResult<>(builder.maxParamCount(), 64);
int localFailures = 0;
startLatch.await(); // wait for all threads to be ready
for (int j = 0; j < iterationsPerThread; j++) {
int pathIndex = (j + threadIndex) % views.length;
ByteView view = views[pathIndex];
String expectedHandler = expectedHandlers[pathIndex];
result.reset();
int routeId = router.match(view, result);
if (expectedHandler == null) {
if (routeId != FastPathRouter.NO_MATCH || result.handler() != null) {
localFailures++;
}
} else {
if (routeId == FastPathRouter.NO_MATCH || !expectedHandler.equals(result.handler())) {
localFailures++;
}
}
}
endLatch.countDown();
return localFailures;
}));
}
// Fire!
startLatch.countDown();
boolean completed = endLatch.await(30, TimeUnit.SECONDS);
assertTrue(completed, "Concurrency test timed out");
int totalFailures = 0;
for (Future<Integer> future : futures) {
try {
totalFailures += future.get();
} catch (Exception e) {
totalFailures++;
}
}
executor.shutdownNow();
// 0 failures means every matching attempt returned the correct route
assertEquals(0, totalFailures, "There were mismatched routes during concurrent access (race condition)");
}
private static final class ByteArrayView implements ByteView {
private final byte[] bytes;
private ByteArrayView(byte[] bytes) {
this.bytes = bytes;
}
@Override
public int length() {
return bytes.length;
}
@Override
public byte byteAt(int index) {
return bytes[index];
}
@Override
public boolean supportsLong() {
return false;
}
@Override
public long longAt(int index) {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,277 @@
package dev.relism.fpr.core;
import dev.relism.fpr.core.dsl.StringRouteParser;
import org.junit.jupiter.api.Test;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class RouterMatchTest {
@Test
void routePrecedencePrefersLiteralMixedParamWildcardCatchAll() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/a/b"), "LITERAL");
builder.add(StringRouteParser.parse("/a/pre-{x}-suf"), "MIXED");
builder.add(StringRouteParser.parse("/a/{id}"), "PARAM");
builder.add(StringRouteParser.parse("/a/*"), "WILD");
builder.add(StringRouteParser.parse("/a/**"), "CATCH");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
assertThat(router.match(view("/a/b"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("LITERAL");
assertThat(router.match(view("/a/pre-123-suf"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("MIXED");
assertThat(router.match(view("/a/zzz"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("PARAM");
assertThat(router.match(view("/a/zzz/extra"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("CATCH");
}
@Test
void mixedSegmentsCaptureParams() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/some{id}"), "A");
builder.add(StringRouteParser.parse("/pre-{x}-suf"), "B");
builder.add(RoutePattern.of(RoutePattern.mixed(new String[]{"", "-suf"}, new String[]{"x"})), "C");
builder.add(RoutePattern.of(RoutePattern.mixed(new String[]{"pre-", ""}, new String[]{"x"})), "D");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
router.match(view("/some123"), out);
assertThat(out.paramCount()).isEqualTo(1);
assertThat(span(view("/some123"), out, 0)).isEqualTo("123");
router.match(view("/pre-abc-suf"), out);
assertThat(out.paramCount()).isEqualTo(1);
assertThat(span(view("/pre-abc-suf"), out, 0)).isEqualTo("abc");
router.match(view("/123-suf"), out);
assertThat(out.paramCount()).isEqualTo(1);
assertThat(out.handler()).isEqualTo("C");
assertThat(span(view("/123-suf"), out, 0)).isEqualTo("123");
router.match(view("/pre-123"), out);
assertThat(out.paramCount()).isEqualTo(1);
assertThat(out.handler()).isEqualTo("D");
assertThat(span(view("/pre-123"), out, 0)).isEqualTo("123");
}
@Test
void mixedEmptyPrefixStillMatches() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(RoutePattern.of(RoutePattern.mixed(new String[]{"", "-verylongsuffix"}, new String[]{"x"})), "A");
builder.add(StringRouteParser.parse("/b{y}"), "B");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
assertThat(router.match(view("/abc-verylongsuffix"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("A");
assertThat(span(view("/abc-verylongsuffix"), out, 0)).isEqualTo("abc");
}
@Test
void fallbackToLowerPrecedenceWhenHigherPathFails() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/a/b/x"), "LIT");
builder.add(StringRouteParser.parse("/a/{id}/y"), "PARAM");
builder.add(StringRouteParser.parse("/a/**"), "CATCH");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount(), 64);
router.match(view("/a/b/y"), out);
assertThat(out.handler()).isEqualTo("PARAM");
router.match(view("/a/123/z"), out);
assertThat(out.handler()).isEqualTo("CATCH");
}
@Test
void indexedLiteralLookupMatchesAcrossStates() {
RouterBuilder<String> builder = new RouterBuilder<>();
for (int i = 0; i < 20; i++) {
builder.add(StringRouteParser.parse("/s0/r" + i), "S0-" + i);
builder.add(StringRouteParser.parse("/s1/r" + i), "S1-" + i);
builder.add(StringRouteParser.parse("/s2/r" + i), "S2-" + i);
}
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount(), 64);
assertThat(router.match(view("/s0/r19"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("S0-19");
assertThat(router.match(view("/s1/r7"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("S1-7");
assertThat(router.match(view("/s2/r3"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("S2-3");
}
@Test
void matchDoesNotReplaceResultArrays() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/a/{id}"), "A");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
int[] keyIds = out.keyIdsArray();
int[] starts = out.startsArray();
int[] lens = out.lensArray();
router.match(view("/a/123"), out);
assertThat(out.keyIdsArray()).isSameAs(keyIds);
assertThat(out.startsArray()).isSameAs(starts);
assertThat(out.lensArray()).isSameAs(lens);
}
@Test
void forEachParamProvidesNamesAndSpans() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "A");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
ByteView view = view("/users/42/orders/7");
assertThat(router.match(view, out)).isNotEqualTo(FastPathRouter.NO_MATCH);
String[] paramNames = builder.paramNames();
List<String> seen = new ArrayList<>();
out.forEachParam(view, paramNames, (name, bytes, start, len) -> {
seen.add(name + "=" + span(bytes, start, len));
});
assertThat(seen).containsExactly("id=42", "orderId=7");
}
@Test
void paramNamesAreOrderedAndUnique() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/a/{id}"), "A");
builder.add(StringRouteParser.parse("/b/{id}/c/{slug}"), "B");
builder.add(StringRouteParser.parse("/c/{slug}/d/{id}"), "C");
assertThat(builder.paramNames()).containsExactly("id", "slug");
}
@Test
void forEachParamRejectsMissingNameEntries() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("/users/{id}/orders/{orderId}"), "A");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
ByteView view = view("/users/42/orders/7");
router.match(view, out);
String[] paramNames = new String[]{"id"};
assertThatThrownBy(() -> out.forEachParam(view, paramNames, (name, bytes, start, len) -> {
})).isInstanceOf(IllegalArgumentException.class);
}
@Test
void labelsDifferentiateSamePath() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add("GET", StringRouteParser.parse("/users"), "GET_HANDLER");
builder.add("POST", StringRouteParser.parse("/users"), "POST_HANDLER");
int getId = builder.labelId("GET");
int postId = builder.labelId("POST");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
out.labelId(getId);
assertThat(router.match(view("/users"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("GET_HANDLER");
out.labelId(postId);
assertThat(router.match(view("/users"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("POST_HANDLER");
}
@Test
void labelZeroMatchesAny() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add("GET", StringRouteParser.parse("/assets"), "GET_ASSETS");
builder.add(StringRouteParser.parse("/assets"), "ANY_ASSETS");
int getId = builder.labelId("GET");
FastPathRouter<ByteView, String> router = builder.compile();
MatchResult<String> out = new MatchResult<>(builder.maxParamCount());
out.labelId(getId);
assertThat(router.match(view("/assets"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("GET_ASSETS");
out.labelId(0);
assertThat(router.match(view("/assets"), out)).isNotEqualTo(FastPathRouter.NO_MATCH);
assertThat(out.handler()).isEqualTo("ANY_ASSETS");
}
private static ByteView view(String path) {
return new ByteArrayView(path.getBytes(StandardCharsets.US_ASCII));
}
private static String span(ByteView view, MatchResult<String> out, int index) {
int start = out.startAt(index);
int len = out.lenAt(index);
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
bytes[i] = view.byteAt(start + i);
}
return new String(bytes, StandardCharsets.US_ASCII);
}
private static String span(ByteView view, int start, int len) {
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
bytes[i] = view.byteAt(start + i);
}
return new String(bytes, StandardCharsets.US_ASCII);
}
private static final class ByteArrayView implements ByteView {
private static final VarHandle LONG_VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, java.nio.ByteOrder.LITTLE_ENDIAN);
private final byte[] bytes;
private ByteArrayView(byte[] bytes) {
this.bytes = bytes;
}
@Override
public int length() {
return bytes.length;
}
@Override
public byte byteAt(int index) {
return bytes[index];
}
@Override
public boolean supportsLong() {
return true;
}
@Override
public long longAt(int index) {
return (long) LONG_VIEW.get(bytes, index);
}
}
}