initial ? wtf
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+86
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
}
|
||||
+160
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user