Working on tree optimizer
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -6,7 +6,7 @@ A constant-time (O(1)) allocator for real-time, high-throughput scenarios. It us
|
|||||||
|
|
||||||
- O(1) allocate/free with 10 levels: 64B x 16MB; each level split into 1x/2x/3x segments
|
- O(1) allocate/free with 10 levels: 64B x 16MB; each level split into 1x/2x/3x segments
|
||||||
- External metadata (`Entry`, `BucketNode`) in dedicated SLABs; user blocks have no headers
|
- External metadata (`Entry`, `BucketNode`) in dedicated SLABs; user blocks have no headers
|
||||||
- Configurable alignment (>=16B); large block cap per allocation: 32MB
|
- Configurable alignment (>=16B); maximum block size is configurable at construction time via the `maxBlockSize` parameter (default 32MB, 10 levels). The level array and bucket table are dynamically extended when a larger `maxBlockSize` is specified. There is no implicit fallback to `NativeMemory` for oversized requests.
|
||||||
- Single-threaded implementation (external sync required for multi-threaded use)
|
- Single-threaded implementation (external sync required for multi-threaded use)
|
||||||
|
|
||||||
## Quick Use
|
## Quick Use
|
||||||
@@ -23,13 +23,13 @@ hlsf.Free(h);
|
|||||||
### Alloc(uint size)
|
### Alloc(uint size)
|
||||||
|
|
||||||
- Purpose: Allocate a block of at least `size` bytes.
|
- Purpose: Allocate a block of at least `size` bytes.
|
||||||
- Input: `size` in bytes (<= 32MB for internal path; >32MB uses native large allocation).
|
- Input: `size` in bytes. Must not exceed the `maxBlockSize` configured at construction time (default 32MB). If `size > _maxSize`, an `InvalidOperationException` is thrown.
|
||||||
- Output: `HybridLayerSegregatedFitAllocationHandle` with pointer access and `Span<byte>`.
|
- Output: `HybridLayerSegregatedFitAllocationHandle` with pointer access and `Span<byte>`.
|
||||||
- Behavior:
|
- Behavior:
|
||||||
- Classify into level/segment and pop a matching bucket; escalate segments and levels if empty.
|
- Classify into level/segment and pop a matching bucket; escalate segments and levels if empty.
|
||||||
- Fallback to overflow bucket or carve 32MB from spare memory if all buckets are empty.
|
- Fallback to overflow bucket or carve 32MB from spare memory if all buckets are empty.
|
||||||
- Mark selected `Entry` as Alloc; slice any remainder into `Empty` entries and push to buckets.
|
- Mark selected `Entry` as Alloc; slice any remainder into `Empty` entries and push to buckets.
|
||||||
- For >32MB requests, use `NativeMemory.AlignedAlloc` and mark `EntryState.Large`.
|
- There is no implicit fallback to `NativeMemory.AlignedAlloc` for oversized requests — all allocations are served from the pre-allocated arena. The level array and bucket table are dynamically sized based on the `maxBlockSize` parameter.
|
||||||
|
|
||||||
### Free(HybridLayerSegregatedFitAllocationHandle handle)
|
### Free(HybridLayerSegregatedFitAllocationHandle handle)
|
||||||
|
|
||||||
@@ -47,34 +47,6 @@ hlsf.Free(h);
|
|||||||
- Consolidate fragmented regions where possible and improve bucket coverage.
|
- Consolidate fragmented regions where possible and improve bucket coverage.
|
||||||
- Intended to be cheap; safe to call between benchmark phases.
|
- Intended to be cheap; safe to call between benchmark phases.
|
||||||
|
|
||||||
## Benchmarks (this run)
|
|
||||||
|
|
||||||
Environment: .NET 8 (Release), x64, single-thread, arena=3072 MiB. Results from StgSharpDebug/HlsfStressTest.
|
|
||||||
|
|
||||||
- Free throughput: 1KB x 262,144 -> 38,928,422.9 free/s
|
|
||||||
- Alloc+free pairs: 256B x 5,000,000 -> 25,006,976.9 ops/s
|
|
||||||
- Fragmentation cycle (~512MB): fast recovery in short cycle (see test for details)
|
|
||||||
|
|
||||||
Representative allocation throughput (higher is better):
|
|
||||||
|
|
||||||
| Size | Ops | alloc/s (no touch) | alloc/s (touch) |
|
|
||||||
|------|----:|--------------------:|----------------:|
|
|
||||||
| 64B | 2,000,000 | 8,780,467.7 | 17,524,920.4 |
|
|
||||||
| 1KB | 1,572,864 | 8,974,865.4 | 9,585,531.6 |
|
|
||||||
| 4KB | 393,216 | 7,340,392.2 | 6,849,319.4 |
|
|
||||||
| 32KB | 49,152 | 10,376,625.6 | 7,019,407.9 |
|
|
||||||
| 256KB | 6,144 | 16,041,775.5 | 12,842,809.4 |
|
|
||||||
| 512KB | 3,072 | 20,466,355.8 | 14,195,933.5 |
|
|
||||||
| 1MB | 1,536 | 16,916,299.6 | 13,568,904.6 |
|
|
||||||
| 2MB | 768 | 17,985,948.5 | 12,569,558.1 |
|
|
||||||
| 8MB | 192 | 13,426,573.4 | 11,034,482.8 |
|
|
||||||
| 32MB | 48 | 11,162,790.7 | 9,411,764.7 |
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Peak no-touch at 2MB (~17.99M alloc/s). Peak touch near 64B (~17.52M alloc/s).
|
|
||||||
- Non-aligned sizes (e.g., 768B, 3KB) typically lower due to slicing/boundary handling.
|
|
||||||
|
|
||||||
### BenchmarkDotNet (HLSF vs Libc)
|
### BenchmarkDotNet (HLSF vs Libc)
|
||||||
|
|
||||||
Environment: .NET 8.0.23 (Release), x64 RyuJIT AVX2, Windows 11, AMD Ryzen 7 6800H.
|
Environment: .NET 8.0.23 (Release), x64 RyuJIT AVX2, Windows 11, AMD Ryzen 7 6800H.
|
||||||
@@ -90,7 +62,7 @@ Interpretation: HLSF matches libc allocation throughput under the same workload,
|
|||||||
|
|
||||||
## Detailed Design (summary)
|
## Detailed Design (summary)
|
||||||
-
|
-
|
||||||
- Size classes: levelSizeArray = [64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216] (64B x 4^level)
|
- Size classes: The default level size array is [64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216] (10 levels, 64B × 4^level). When `maxBlockSize` is specified at construction and exceeds the default range, additional levels are appended (each 4× the previous) to cover the requested maximum block size.
|
||||||
- Segments per level: 1x, 2x, 3x
|
- Segments per level: 1x, 2x, 3x
|
||||||
- Buckets: index = 3 * level + segment (overflow bucket for level > 9)
|
- Buckets: index = 3 * level + segment (overflow bucket for level > 9)
|
||||||
- Metadata: Entry (external SLAB, position chain), BucketNode (user region, bucket chain)
|
- Metadata: Entry (external SLAB, position chain), BucketNode (user region, bucket chain)
|
||||||
@@ -143,8 +115,8 @@ Interpretation: HLSF matches libc allocation throughput under the same workload,
|
|||||||
|
|
||||||
### Large Allocations and Spare Memory
|
### Large Allocations and Spare Memory
|
||||||
|
|
||||||
- Requests > 32MB use `NativeMemory.AlignedAlloc(size, align)` and mark `State.Large`
|
- Requests exceeding the configured `maxBlockSize` are rejected with `InvalidOperationException`. All allocations are served from the pre-allocated arena.
|
||||||
- When buckets are empty, `TryAllocateNew32MBBlock` carves a 32MB region from `_spareMemory`, builds an `Empty` entry, and advances the spare pointer
|
- When all buckets are empty, `TryAllocateNew32MBBlock` carves a region (up to 32MB) from `_spareMemory` to replenish the bucket system, builds an `Empty` entry, and advances the spare pointer
|
||||||
|
|
||||||
### Alignment
|
### Alignment
|
||||||
|
|
||||||
|
|||||||
@@ -181,9 +181,20 @@ namespace StgSharp.RegularAnalysis.Abstraction
|
|||||||
_state = 0;
|
_state = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SkipNode()
|
/// <summary>
|
||||||
|
/// Skips the node and its subtree if the current node is the given node. Otherwise, do
|
||||||
|
/// nothing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">
|
||||||
|
/// The node to skip.
|
||||||
|
/// </param>
|
||||||
|
public void SkipNode(
|
||||||
|
TNode node
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (_stack.TryPop(out Record? r)) {
|
if (_stack.TryPeek(out Record? r) && ReferenceEquals(r.Node, node))
|
||||||
|
{
|
||||||
|
_ = _stack.Pop();
|
||||||
_recycle.Push(r);
|
_recycle.Push(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ namespace StgSharp.RegularAnalysis.Abstraction
|
|||||||
|
|
||||||
#region depth
|
#region depth
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void DecreaseDepth(
|
public void DecreaseDepth(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -185,6 +186,7 @@ namespace StgSharp.RegularAnalysis.Abstraction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void IncreaseDepth(
|
public void IncreaseDepth(
|
||||||
int mark
|
int mark
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,14 +66,6 @@ namespace StgSharp.RegularAnalysis.Abstraction
|
|||||||
TNode nextToken
|
TNode nextToken
|
||||||
);
|
);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static virtual bool IsNullOrEmpty(
|
|
||||||
TNode node
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return node is null || node.Equals(TNode.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PrependNode(
|
void PrependNode(
|
||||||
TNode previousNode
|
TNode previousNode
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ using System.Linq;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace StgSharp.RegularAnalysis.TextRegex
|
namespace StgSharp.RegularAnalysis.TextRegex
|
||||||
{
|
{
|
||||||
@@ -52,10 +53,8 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
|
|
||||||
public long NodeFlag => (long)Source.Flag;
|
public long NodeFlag => (long)Source.Flag;
|
||||||
|
|
||||||
public static RegexAstNode Empty
|
public static RegexAstNode Empty { get; }
|
||||||
{
|
= new EmptyRegexAstNode();
|
||||||
get => new EmptyRegexAstNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public RegexAstNode Previous { get; set; }
|
public RegexAstNode Previous { get; set; }
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
get;
|
get;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
field?.Parent = null;
|
field?.Parent = Empty;
|
||||||
field = value;
|
field = value;
|
||||||
value.Parent = this;
|
value.Parent = this;
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,7 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
get;
|
get;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
field?.Parent = null;
|
field?.Parent = Empty;
|
||||||
field = value;
|
field = value;
|
||||||
value.Parent = this;
|
value.Parent = this;
|
||||||
}
|
}
|
||||||
@@ -90,13 +89,19 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
get => Source.Flag;
|
get => Source.Flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string Value => Source.Value;
|
||||||
|
|
||||||
public string CodeConvertTemplate { get; }
|
public string CodeConvertTemplate { get; }
|
||||||
|
|
||||||
public Token<RegexElementLabel> Source { get; }
|
public Token<RegexElementLabel> Source { get; }
|
||||||
|
|
||||||
public void AppendNode(
|
public void AppendNode(
|
||||||
RegexAstNode nextToken
|
RegexAstNode nextToken
|
||||||
) { }
|
)
|
||||||
|
{
|
||||||
|
Next = nextToken;
|
||||||
|
nextToken.Previous = this;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static (bool Valid,int Value) ComparePrecedence(
|
public static (bool Valid,int Value) ComparePrecedence(
|
||||||
@@ -113,9 +118,33 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
return (true,v_left - v_right);
|
return (true,v_left - v_right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsNullOrEmpty(
|
||||||
|
RegexAstNode node
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return node is null || node.Equals(Empty);
|
||||||
|
}
|
||||||
|
|
||||||
public void PrependNode(
|
public void PrependNode(
|
||||||
RegexAstNode previousNode
|
RegexAstNode previousNode
|
||||||
) { }
|
)
|
||||||
|
{
|
||||||
|
Previous = previousNode;
|
||||||
|
previousNode.Next = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveChild(
|
||||||
|
RegexAstNode node
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (Left == node)
|
||||||
|
{
|
||||||
|
Left = Empty;
|
||||||
|
} else if (Right == node) {
|
||||||
|
Right = Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TryMergeUnit(
|
public static bool TryMergeUnit(
|
||||||
RegexAstNode prev,
|
RegexAstNode prev,
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
using StgSharp.RegularAnalysis.Abstraction;
|
using StgSharp.RegularAnalysis.Abstraction;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -43,9 +45,118 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
{
|
{
|
||||||
TreeEnumerator<RegexAstNode, RegexElementLabel> enumerator = new(root);
|
TreeEnumerator<RegexAstNode, RegexElementLabel> enumerator = new(root);
|
||||||
List<RegexAstNode> cases = [];
|
List<RegexAstNode> cases = [];
|
||||||
|
RegexAstNode current;
|
||||||
|
_ = enumerator.MoveNext(); // skip root
|
||||||
while (enumerator.MoveNext())
|
while (enumerator.MoveNext())
|
||||||
{
|
{
|
||||||
RegexAstNode current = enumerator.Current;
|
current = enumerator.Current;
|
||||||
|
current.Parent.RemoveChild(current);
|
||||||
|
current.Parent = RegexAstNode.Empty;
|
||||||
|
if (current.Parent.EqualityTypeConvert == RegexElementLabel.ALT)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
cases.Add(current);
|
||||||
|
enumerator.SkipNode(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < cases.Count - 1; i++)
|
||||||
|
{
|
||||||
|
cases[i].AppendNode(cases[i + 1]);
|
||||||
|
cases[i].Parent = root;
|
||||||
|
}
|
||||||
|
return cases[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MergeConcat(
|
||||||
|
RegexAstNode root
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (root.EqualityTypeConvert != RegexElementLabel.CONCAT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<string> buffer = [];
|
||||||
|
int length = 0;
|
||||||
|
RegexAstNode right = root.Right;
|
||||||
|
RegexAstNode left = root.Left;
|
||||||
|
if (left.EqualityTypeConvert != RegexElementLabel.UNIT_SPAN &&
|
||||||
|
left.EqualityTypeConvert != RegexElementLabel.UNIT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string str = left.Value;
|
||||||
|
buffer.Add(str);
|
||||||
|
length += str.Length;
|
||||||
|
while (RegexAstNode.IsNullOrEmpty(right) &&
|
||||||
|
right.EqualityTypeConvert == RegexElementLabel.CONCAT)
|
||||||
|
{
|
||||||
|
left = right.Left;
|
||||||
|
if (left.EqualityTypeConvert != RegexElementLabel.UNIT_SPAN &&
|
||||||
|
left.EqualityTypeConvert != RegexElementLabel.UNIT)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
str = left.Value;
|
||||||
|
buffer.Add(str);
|
||||||
|
length += str.Length;
|
||||||
|
root.Right = right.Right;
|
||||||
|
|
||||||
|
// TODO: remove right from tree
|
||||||
|
right = root.Right;
|
||||||
|
}
|
||||||
|
str = string.Create(length, buffer, static(
|
||||||
|
span,
|
||||||
|
buffer
|
||||||
|
) => {
|
||||||
|
int offset = 0;
|
||||||
|
for (int i = 0; i < buffer.Count; i++)
|
||||||
|
{
|
||||||
|
buffer[i].AsSpan().CopyTo(span[offset..]);
|
||||||
|
offset += buffer[i].Length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
left = root.Left;
|
||||||
|
root.Left = new RegexAstNode(new Token<RegexElementLabel>(str, left.Source.Line,
|
||||||
|
left.Source.Column,
|
||||||
|
str.Length == 1 ?
|
||||||
|
RegexElementLabel.UNIT :
|
||||||
|
RegexElementLabel.UNIT_SPAN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RotateConcat(
|
||||||
|
RegexAstNode root
|
||||||
|
)
|
||||||
|
{
|
||||||
|
RegexAstNode left = root.Left;
|
||||||
|
while (RegexAstNode.IsNullOrEmpty(left) &&
|
||||||
|
left.EqualityTypeConvert == RegexElementLabel.CONCAT)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* root → root
|
||||||
|
* ╱ ╲ ╱ ╲
|
||||||
|
* left c a left
|
||||||
|
* ╱ ╲ ╱ ╲
|
||||||
|
* a b b c
|
||||||
|
*/
|
||||||
|
RegexAstNode a = left.Left;
|
||||||
|
RegexAstNode b = left.Right;
|
||||||
|
RegexAstNode c = root.Right;
|
||||||
|
root.Left = a;
|
||||||
|
if (RegexAstNode.IsNullOrEmpty(a)) {
|
||||||
|
a.Parent = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.Right = left;
|
||||||
|
left.Left = b;
|
||||||
|
left.Right = c;
|
||||||
|
if (RegexAstNode.IsNullOrEmpty(b)) {
|
||||||
|
b.Parent = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RegexAstNode.IsNullOrEmpty(c)) {
|
||||||
|
c.Parent = left;
|
||||||
|
}
|
||||||
|
left = root.Left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,24 +174,37 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
{
|
{
|
||||||
// phase0: scan container and remove no parent nodes
|
// phase0: scan container and remove no parent nodes
|
||||||
_tree.AllNodes.RemoveWhere(node => node.Parent == null && node != _tree.Root);
|
_tree.AllNodes.RemoveWhere(node => node.Parent == null && node != _tree.Root);
|
||||||
HashSet<RegexAstNode>.Enumerator enumerator = _tree.AllNodes.GetEnumerator();
|
TreeEnumerator<RegexAstNode, RegexElementLabel> enumerator = new TreeEnumerator<RegexAstNode, RegexElementLabel>(_tree.Root);
|
||||||
|
RegexAstNode current;
|
||||||
|
|
||||||
// phase1: ALT flatten
|
// phase1: ALT flatten
|
||||||
|
// phase2: CONCAT normalize
|
||||||
while (enumerator.MoveNext())
|
while (enumerator.MoveNext())
|
||||||
{
|
{
|
||||||
RegexAstNode node = enumerator.Current;
|
current = enumerator.Current;
|
||||||
if ((RegexElementLabel)node.NodeFlag == RegexElementLabel.ALT)
|
if (current.EqualityTypeConvert == RegexElementLabel.ALT)
|
||||||
{
|
{
|
||||||
RegexAstNode list = FlattenAlt(node);
|
RegexAstNode union = FlattenAlt(current);
|
||||||
node.Left = list;
|
current.Right = union;
|
||||||
node.Right = RegexAstNode.Empty;
|
current.Left = RegexAstNode.Empty;
|
||||||
|
} else if (current.EqualityTypeConvert == RegexElementLabel.CONCAT) {
|
||||||
|
RotateConcat(current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// phase2: ALT merge
|
|
||||||
|
|
||||||
|
|
||||||
// phase3: CONCAT merge
|
// phase3: CONCAT merge
|
||||||
|
enumerator.Reset();
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
// suppose all concat is now in normalized form,
|
||||||
|
// we can merge adjacent unit nodes into a single unit node with string value
|
||||||
|
current = enumerator.Current;
|
||||||
|
if (current.EqualityTypeConvert == RegexElementLabel.CONCAT) {
|
||||||
|
MergeConcat(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase4: ALT union
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessOperator(
|
private void ProcessOperator(
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace StgSharp.RegularAnalysis.TextRegex
|
|||||||
}
|
}
|
||||||
position = pos + 2;
|
position = pos + 2;
|
||||||
return new Token<RegexElementLabel>(_source[(pos)..(pos + 2)], 0, pos,
|
return new Token<RegexElementLabel>(_source[(pos)..(pos + 2)], 0, pos,
|
||||||
RegexElementLabel.UNIT);
|
RegexElementLabel.UNIT_SET);
|
||||||
case '[':
|
case '[':
|
||||||
for (int i = pos + 1; i < _source.Length; i++)
|
for (int i = pos + 1; i < _source.Length; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user