From 78b84199d888225ffc3c20d65e865c82a29e5d7c Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sat, 10 Jun 2023 13:15:37 +0100 Subject: [PATCH] Improve parsing performance - In LinguiniParser, ZeroCopyReader, ZeroCopyUtil: change helpers from dealing in ReadOnlyMemory to char. In inner loops this removes a lot of redundant checks for a span of length 1 and inlines more readily. - In ZeroCopyReader add helpers for SeekEol, IndexOfAnyChar. These use existing helper methods of Span that are SIMD accelerated to search for indexes quickly. These provide significant speedups compared to incrementing the index in a loop. - In FluentBundle.AddResourceOverriding, provide a specialized 'override' method to avoid two dictionary lookups and allocating an empty list. --- Linguini.Bundle/FluentBundle.cs | 12 +- Linguini.Shared/Util/ZeroCopyUtil.cs | 134 +------ .../IO/NonValidCharInputTest.cs | 14 +- .../IO/ZeroCopyReaderTest.cs | 47 ++- Linguini.Syntax/IO/ZeroCopyReader.cs | 90 +++-- Linguini.Syntax/Parser/Error/ParseError.cs | 4 +- Linguini.Syntax/Parser/LinguiniParser.cs | 340 +++++++++--------- 7 files changed, 295 insertions(+), 346 deletions(-) diff --git a/Linguini.Bundle/FluentBundle.cs b/Linguini.Bundle/FluentBundle.cs index 65ae274..8d172c0 100644 --- a/Linguini.Bundle/FluentBundle.cs +++ b/Linguini.Bundle/FluentBundle.cs @@ -126,15 +126,21 @@ public void AddResourceOverriding(Resource res) if (entry is AstTerm or AstMessage) { - AddEntry(new List(), entry, true); + AddEntryOverriding(entry); } } } - private void AddEntry(List errors, IEntry term, bool overwrite = false) + private void AddEntryOverriding(IEntry term) { var id = (term.GetId(), term.ToKind()); - if (_entries.ContainsKey(id) && !overwrite) + _entries[id] = term; + } + + private void AddEntry(List errors, IEntry term) + { + var id = (term.GetId(), term.ToKind()); + if (_entries.ContainsKey(id)) { errors.Add(new OverrideFluentError(id.Item1, id.Item2)); } diff --git a/Linguini.Shared/Util/ZeroCopyUtil.cs b/Linguini.Shared/Util/ZeroCopyUtil.cs index b515b6f..5b12732 100644 --- a/Linguini.Shared/Util/ZeroCopyUtil.cs +++ b/Linguini.Shared/Util/ZeroCopyUtil.cs @@ -9,123 +9,51 @@ namespace Linguini.Shared.Util /// public static class ZeroCopyUtil { - private const int CharLength = 1; - private static readonly ReadOnlyMemory Eof = ReadOnlyMemory.Empty; - - public static ReadOnlySpan PeakCharAt(this ReadOnlyMemory memory, int pos) - { - memory.TryReadCharSpan(pos, out var span); - return span; - } - - public static bool TryReadCharSpan(this ReadOnlyMemory memory, int pos, out ReadOnlySpan span) + public static bool TryReadChar(this ReadOnlyMemory memory, int pos, out char c) { - span = Eof.Span; - if (pos + CharLength > memory.Length) + if (pos >= memory.Length) { + c = default; return false; } - span = memory.Slice(pos, CharLength).Span; + c = memory.Span[pos]; return true; } - public static bool EqualsSpans(this char chr, ReadOnlySpan chrSpan) - { - if (chrSpan.Length > CharLength) - { - return false; - } - - return chrSpan.Length != 0 && IsEqual(chrSpan, chr); - } - - public static bool EqualsSpans(this char? lhs, ReadOnlySpan chrSpan) - { - if (lhs == null) - { - return chrSpan == Eof.Span; - } - - return EqualsSpans((char) lhs, chrSpan); - } - - public static bool IsEqual(this ReadOnlySpan charSpan, char c1) - { - if (charSpan.Length != CharLength) - { - return false; - } - - return MemoryMarshal.GetReference(charSpan) == c1; - } - - public static bool IsAsciiAlphabetic(this ReadOnlySpan charSpan) + public static bool IsAsciiAlphabetic(this char c) { - if (charSpan.Length != CharLength) - { - return false; - } - - var c = MemoryMarshal.GetReference(charSpan); return IsInside(c, 'a', 'z') || IsInside(c, 'A', 'Z'); } - public static bool IsAsciiHexdigit(this ReadOnlySpan charSpan) + public static bool IsAsciiHexdigit(this char c) { - if (charSpan.Length != CharLength) - { - return false; - } - - var c = MemoryMarshal.GetReference(charSpan); return IsInside(c, '0', '9') || IsInside(c, 'a', 'f') || IsInside(c, 'A', 'F'); } - public static bool IsAsciiUppercase(this ReadOnlySpan charSpan) + public static bool IsAsciiUppercase(this char c) { - if (charSpan.Length != CharLength) - { - return false; - } - - var c = MemoryMarshal.GetReference(charSpan); return IsInside(c, 'A', 'Z'); } - public static bool IsAsciiDigit(this ReadOnlySpan charSpan) + public static bool IsAsciiDigit(this char c) { - if (charSpan.Length != CharLength) - { - return false; - } - - var c = MemoryMarshal.GetReference(charSpan); return IsInside(c, '0', '9'); } - - - public static bool IsNumberStart(this ReadOnlySpan charSpan) + public static bool IsNumberStart(this char c) { - if (charSpan.Length != CharLength) - { - return false; - } - - var c = MemoryMarshal.GetReference(charSpan); return IsInside(c, '0', '9') || c == '-'; } - public static bool IsCallee(this ReadOnlyMemory charSpan) + public static bool IsCallee(this ReadOnlySpan charSpan) { bool isCallee = true; - for (int i = 0; i < charSpan.Length - 1; i++) + foreach (var c in charSpan) { - var c = charSpan.Slice(i, 1).Span; if (!(c.IsAsciiUppercase() || c.IsAsciiDigit() || c.IsOneOf('_', '-'))) { isCallee = false; @@ -136,53 +64,29 @@ public static bool IsCallee(this ReadOnlyMemory charSpan) return isCallee; } - - public static bool IsIdentifier(this ReadOnlySpan charSpan) + public static bool IsIdentifier(this char c) { - if (charSpan.Length != CharLength) - { - return false; - } - - var c = MemoryMarshal.GetReference(charSpan); return IsInside(c, 'a', 'z') || IsInside(c, 'A', 'Z') || IsInside(c, '0', '9') || c == '-' || c == '_'; } - public static bool IsOneOf(this ReadOnlySpan charSpan, char c1, char c2) + public static bool IsOneOf(this char c, char c1, char c2) { - if (charSpan.Length != CharLength) - { - return false; - } - - var x = MemoryMarshal.GetReference(charSpan); - return x == c1 || x == c2; + return c == c1 || c == c2; } - public static bool IsOneOf(this ReadOnlySpan charSpan, char c1, char c2, char c3) + public static bool IsOneOf(this char c, char c1, char c2, char c3) { - if (charSpan.Length != CharLength) - { - return false; - } - - var x = MemoryMarshal.GetReference(charSpan); - return x == c1 || x == c2 || x == c3; + return c == c1 || c == c2 || c == c3; } - public static bool IsOneOf(this ReadOnlySpan charSpan, char c1, char c2, char c3, char c4) + public static bool IsOneOf(this char c, char c1, char c2, char c3, char c4) { - if (charSpan.Length != CharLength) - { - return false; - } - - var x = MemoryMarshal.GetReference(charSpan); - return x == c1 || x == c2 || x == c3 || x == c4; + return c == c1 || c == c2 || c == c3 || c == c4; } + #if !NET5_0_OR_GREATER // Polyfill for netstandard 2.1 until dotnet backports MemoryExtension public static ReadOnlyMemory TrimEndPolyFill(this ReadOnlyMemory memory) diff --git a/Linguini.Syntax.Tests/IO/NonValidCharInputTest.cs b/Linguini.Syntax.Tests/IO/NonValidCharInputTest.cs index 20bc89c..975c1fc 100644 --- a/Linguini.Syntax.Tests/IO/NonValidCharInputTest.cs +++ b/Linguini.Syntax.Tests/IO/NonValidCharInputTest.cs @@ -14,20 +14,10 @@ public class NonValidCharInputTest [TestCase("단편")] [TestCase("かんじ")] [TestCase("Северный поток")] - public void OperationOnNonCharSpanReturnFalse(string input) + public void OperationOnNonCalleeReturnFalse(string input) { var span = input.AsSpan(); - Assert.False('c'.EqualsSpans(span)); - Assert.False(span.IsEqual('c')); - Assert.False(span.IsAsciiAlphabetic()); - Assert.False(span.IsAsciiDigit()); - Assert.False(span.IsAsciiHexdigit()); - Assert.False(span.IsAsciiUppercase()); - Assert.False(span.IsOneOf('c', 's')); - Assert.False(span.IsOneOf('c', 's', 'l')); - Assert.False(span.IsOneOf('c', 's', 'a', 'l')); - Assert.False(span.IsNumberStart()); - Assert.False(input.AsMemory().IsCallee()); + Assert.False(span.IsCallee()); } } } diff --git a/Linguini.Syntax.Tests/IO/ZeroCopyReaderTest.cs b/Linguini.Syntax.Tests/IO/ZeroCopyReaderTest.cs index 3656179..f2ff193 100644 --- a/Linguini.Syntax.Tests/IO/ZeroCopyReaderTest.cs +++ b/Linguini.Syntax.Tests/IO/ZeroCopyReaderTest.cs @@ -20,9 +20,9 @@ public class ZeroCopyReaderTest public void TestPeekChar(string text, char expected) { ZeroCopyReader reader = new ZeroCopyReader(text); - Assert.That(expected.EqualsSpans(reader.PeekCharSpan())); - Assert.That(expected.EqualsSpans(reader.PeekCharSpan())); - Assert.That(expected.EqualsSpans(reader.PeekCharSpan())); + Assert.That(expected == reader.PeekChar()); + Assert.That(expected == reader.PeekChar()); + Assert.That(expected == reader.PeekChar()); } @@ -43,7 +43,7 @@ public void TestExpectChar(string text, char expectedChr, bool expected, char? p { ZeroCopyReader reader = new ZeroCopyReader(text); Assert.AreEqual(expected, reader.ReadCharIf(expectedChr)); - Assert.True(peek.EqualsSpans(reader.PeekCharSpan())); + Assert.True(peek == reader.PeekChar()); } [Test] @@ -56,9 +56,9 @@ public void TestExpectChar(string text, char expectedChr, bool expected, char? p public void TestPeekGetChar(string text, char expected1, char expected2) { ZeroCopyReader reader = new ZeroCopyReader(text); - Assert.That(expected1.EqualsSpans(reader.PeekCharSpan())); - Assert.That(expected1.EqualsSpans(reader.GetCharSpan())); - Assert.That(expected2.EqualsSpans(reader.PeekCharSpan())); + Assert.That(expected1 == reader.PeekChar()); + reader.Position += 1; + Assert.That(expected2 == reader.PeekChar()); } [Test] @@ -71,8 +71,8 @@ public void TestPeekGetChar(string text, char expected1, char expected2) public void TestPeekCharOffset(string text, char expected1, char expected2) { ZeroCopyReader reader = new ZeroCopyReader(text); - Assert.That(expected1.EqualsSpans(reader.PeekCharSpan())); - Assert.That(expected2.EqualsSpans(reader.PeekCharSpan(1))); + Assert.That(expected1 == reader.PeekChar()); + Assert.That(expected2 == reader.PeekChar(1)); } [Test] @@ -86,19 +86,38 @@ public void TestSkipBlank(string text, char postSkipChar) { ZeroCopyReader reader = new ZeroCopyReader(text); reader.SkipBlankBlock(); - Assert.That(postSkipChar.EqualsSpans(reader.GetCharSpan())); + Assert.That(postSkipChar == reader.PeekChar()); } [Test] [Parallelizable] - [TestCase("", false, null)] + [TestCase(" \nb", true, 3, 2)] + [TestCase(" \r\nb2", true, 5, 2)] + [TestCase(" \n漢字", true, 5, 2)] + [TestCase(" \n단편", true, 6, 2)] + [TestCase(" \nか", true, 7, 2)] + [TestCase(" \rか", false, 8, 1)] + [TestCase("", false, 0, 1)] + [TestCase("bbbbb", false, 5, 1)] + public void TestSeekEol(string text, bool expectedEol, int expectedPosition, int expectedRow) + { + ZeroCopyReader reader = new ZeroCopyReader(text); + var foundEol = reader.SeekEol(); + Assert.That(expectedEol, Is.EqualTo(foundEol)); + Assert.That(expectedPosition, Is.EqualTo(reader.Position)); + Assert.That(expectedRow, Is.EqualTo(reader.Row)); + } + + [Test] + [Parallelizable] + [TestCase("", false, default(char))] [TestCase("a", true, 'a')] - public void TestTryReadCharSpan(string text, bool isChar, char? expected1) + public void TestTryReadChar(string text, bool isChar, char? expected1) { ReadOnlyMemory mem = new ReadOnlyMemory(text.ToCharArray()); - bool isThereChar = mem.TryReadCharSpan(0, out var readChr); + bool isThereChar = mem.TryReadChar(0, out var readChr); Assert.That(isThereChar, Is.EqualTo(isChar)); - Assert.That(expected1.EqualsSpans(readChr)); + Assert.That(expected1 == readChr); } [Test] diff --git a/Linguini.Syntax/IO/ZeroCopyReader.cs b/Linguini.Syntax/IO/ZeroCopyReader.cs index 0c14952..82c81ca 100644 --- a/Linguini.Syntax/IO/ZeroCopyReader.cs +++ b/Linguini.Syntax/IO/ZeroCopyReader.cs @@ -38,27 +38,14 @@ public int Row public bool IsEof => !IsNotEof; internal ReadOnlyMemory GetData => _unconsumedData; - public ReadOnlySpan PeekCharSpan(int offset = 0) + public char? PeekChar(int offset = 0) { - return _unconsumedData.PeakCharAt(_position + offset); - } - - public bool IsCurrentChar(char c) - { - return c.EqualsSpans(_unconsumedData.PeakCharAt(_position)); - } - - public ReadOnlySpan PeekCharSpanAt(int pos) - { - return _unconsumedData.PeakCharAt(pos); - } - + if (_unconsumedData.TryReadChar(_position + offset, out var c)) + { + return c; + } - public ReadOnlySpan GetCharSpan() - { - var chr = PeekCharSpan(); - _position += 1; - return chr; + return null; } public int SkipBlankBlock() @@ -80,17 +67,31 @@ public int SkipBlankBlock() return count; } + public bool SeekEol() + { + var index = _unconsumedData.Span.Slice(_position).IndexOf('\n'); + if (index != -1) + { + _row += 1; + _position += index + 1; + return true; + } + + _position = _unconsumedData.Length; + return false; + } + public bool SkipEol() { - if ('\n'.EqualsSpans(PeekCharSpan())) + if ('\n' == PeekChar()) { _row += 1; _position += 1; return true; } - if ('\r'.EqualsSpans(PeekCharSpan()) - && '\n'.EqualsSpans(PeekCharSpan(1))) + if ('\r' == PeekChar() + && '\n' == PeekChar(1)) { _row += 1; _position += 2; @@ -103,7 +104,7 @@ public bool SkipEol() public int SkipBlankInline() { var start = _position; - while (' '.EqualsSpans(PeekCharSpan())) + while (_unconsumedData.TryReadChar(_position, out var c) && ' ' == c) { _position += 1; } @@ -113,7 +114,7 @@ public int SkipBlankInline() public bool ReadCharIf(char c) { - if (c.EqualsSpans(PeekCharSpan())) + if (_unconsumedData.TryReadChar(_position, out var c1) && c == c1) { if (c == '\n') { @@ -141,11 +142,11 @@ public ReadOnlyMemory GetCommentLine() private bool IsEol() { - var chr = PeekCharSpan(); + var chr = PeekChar(); - if ('\n'.EqualsSpans(chr)) return true; - if ('\r'.EqualsSpans(chr) - && '\n'.EqualsSpans(PeekCharSpan(1))) + if ('\n' == chr) return true; + if ('\r' == chr + && '\n' == PeekChar(1)) { return true; } @@ -165,15 +166,15 @@ public string ReadSliceToStr(int start, int end) public void SkipToNextEntry() { - while (_unconsumedData.TryReadCharSpan(_position, out var span)) + while (_unconsumedData.TryReadChar(_position, out var c)) { var newline = _position == 0 - || '\n'.EqualsSpans(PeekCharSpan(-1)); + || '\n' == PeekChar(-1); if (newline) { _row += 1; - if (span.IsAsciiAlphabetic() || span.IsOneOf('#', '-')) + if (c.IsAsciiAlphabetic() || c.IsOneOf('#', '-')) break; } @@ -181,25 +182,40 @@ public void SkipToNextEntry() } } - public bool TryPeekCharSpan(out ReadOnlySpan span) + public int IndexOfAnyChar(ReadOnlySpan values) + { + return _unconsumedData.Span.Slice(_position).IndexOfAny(values); + } + + public bool TryPeekChar(out char c) + { + return _unconsumedData.TryReadChar(_position, out c); + } + + public bool TryPeekCharAt(int pos, out char c) + { + return _unconsumedData.TryReadChar(pos, out c); + } + + public char CurrentChar() { - return _unconsumedData.TryReadCharSpan(_position, out span); + return _unconsumedData.Span[_position]; } public void SkipBlank() { - while (TryPeekCharSpan(out var span)) + while (TryPeekChar(out var c)) { - if (' '.EqualsSpans(span)) + if (' ' == c) { _position += 1; } - else if ('\n'.EqualsSpans(span)) + else if ('\n' == c) { _position += 1; _row += 1; } - else if ('\r'.EqualsSpans(span) && '\n'.EqualsSpans(PeekCharSpan(1))) + else if ('\r' == c && '\n' == PeekChar(1)) { _position += 2; _row += 1; diff --git a/Linguini.Syntax/Parser/Error/ParseError.cs b/Linguini.Syntax/Parser/Error/ParseError.cs index 6defe99..f0476e7 100644 --- a/Linguini.Syntax/Parser/Error/ParseError.cs +++ b/Linguini.Syntax/Parser/Error/ParseError.cs @@ -22,7 +22,7 @@ private ParseError(ErrorType kind, string message, Range position, int row) Row = row; } - public static ParseError ExpectedToken(char expected, ReadOnlySpan actual, int pos, int row) + public static ParseError ExpectedToken(char expected, char? actual, int pos, int row) { var sb = new StringBuilder() .AppendFormat("Expected a token starting with \"{0}\" found \"{1}\" instead", expected, @@ -36,7 +36,7 @@ public static ParseError ExpectedToken(char expected, ReadOnlySpan actual, ); } - public static ParseError ExpectedToken(char exp1, char exp2, ReadOnlySpan actual, int pos, int row) + public static ParseError ExpectedToken(char exp1, char exp2, char? actual, int pos, int row) { var sb = new StringBuilder() .AppendFormat("Expected \"{0}\" or \"{1}\" found \"{2}\" instead", diff --git a/Linguini.Syntax/Parser/LinguiniParser.cs b/Linguini.Syntax/Parser/LinguiniParser.cs index 36b6e51..e736cad 100644 --- a/Linguini.Syntax/Parser/LinguiniParser.cs +++ b/Linguini.Syntax/Parser/LinguiniParser.cs @@ -79,15 +79,9 @@ public Resource Parse() private void SkipComment() { - while (_reader.IsNotEof) + while (_reader.SeekEol()) { - while (_reader.IsNotEof - && !_reader.SkipEol()) - { - _reader.Position += 1; - } - - if (_reader.PeekCharSpan().IsEqual('#')) + if (_reader.TryPeekChar(out var c) && c == '#') { _reader.Position += 1; } @@ -100,16 +94,18 @@ private void SkipComment() private (IEntry?, ParseError?) GetEntryRuntime(int entryStart) { - var chrSpan = _reader.PeekCharSpan(); - if (chrSpan.IsEqual('#')) + if (_reader.TryPeekChar(out var c)) { - SkipComment(); - return (null, null); - } + if (c == '#') + { + SkipComment(); + return (null, null); + } - if (chrSpan.IsEqual('-')) - { - return GetTerm(entryStart); + if (c == '-') + { + return GetTerm(entryStart); + } } return GetMessage(entryStart); @@ -194,9 +190,9 @@ private void AddError(ParseError error, int entryStart, List errors, private (IEntry, ParseError?) GetEntry(int entryStart) { - var charSpan = _reader.PeekCharSpan(); + var charSpan = _reader.PeekChar(); - if ('#'.EqualsSpans(charSpan)) + if ('#' == charSpan) { IEntry entry = new Junk(); if (TryGetComment(out var comment, out var error)) @@ -207,7 +203,7 @@ private void AddError(ParseError error, int entryStart, List errors, return (entry, error); } - if ('-'.EqualsSpans(charSpan)) + if ('-' == charSpan) { return GetTerm(entryStart); } @@ -322,8 +318,8 @@ private bool TryGetComment([NotNullWhen(true)] out AstComment? comment, out Pars break; } - if ('\n'.EqualsSpans(_reader.PeekCharSpan()) - || ('\r'.EqualsSpans(_reader.PeekCharSpan()) && '\n'.EqualsSpans(_reader.PeekCharSpan(1)))) + if ('\n' == _reader.PeekChar() + || ('\r' == _reader.PeekChar() && '\n' == _reader.PeekChar(1))) { content.Add(_reader.GetCommentLine()); } @@ -389,13 +385,13 @@ private bool TryExpectChar(char c, out ParseError? error) return true; } - error = ParseError.ExpectedToken(c, _reader.PeekCharSpan(), _reader.Position, _reader.Row); + error = ParseError.ExpectedToken(c, _reader.PeekChar(), _reader.Position, _reader.Row); return false; } private bool TryGetIdentifier([NotNullWhen(true)] out Identifier? id, out ParseError? error) { - if (_reader.PeekCharSpan().IsAsciiAlphabetic()) + if (_reader.TryPeekChar(out var c) && c.IsAsciiAlphabetic()) { _reader.Position += 1; error = null; @@ -412,7 +408,7 @@ private Identifier GetUncheckedIdentifier() { // First character is already checked var ptr = _reader.Position; - while (_reader.PeekCharSpanAt(ptr).IsIdentifier()) + while (_reader.TryPeekCharAt(ptr, out var c) && c.IsIdentifier()) { ptr += 1; } @@ -467,11 +463,11 @@ private bool TryGetPattern(out Pattern? pattern, out ParseError? error) if (textElementRole == TextElementPosition.LineStart) { indent = _reader.SkipBlankInline(); - if (_reader.TryPeekCharSpan(out var b)) + if (_reader.TryPeekChar(out var b)) { if (indent == 0) { - if (!'\r'.EqualsSpans(b) && !'\n'.EqualsSpans(b)) + if ('\r' != b && '\n' != b) { break; } @@ -602,13 +598,22 @@ private bool TryGetTextSlice([NotNullWhen(true)] out TextSlice? textElement, out var startPos = _reader.Position; var textElementType = TextElementType.Blank; - while (_reader.TryPeekCharSpan(out var span)) + int index; + while ((index = _reader.IndexOfAnyChar(" \n\r{}".AsSpan())) != -1) { - if (' '.EqualsSpans(span)) + if (index > 0) + { + textElementType = TextElementType.NonBlank; + } + + _reader.Position += index; + var c = _reader.CurrentChar(); + + if (' ' == c) { _reader.Position += 1; } - else if ('\n'.EqualsSpans(span)) + else if ('\n' == c) { _reader.Position += 1; textElement = new TextSlice( @@ -620,8 +625,8 @@ private bool TryGetTextSlice([NotNullWhen(true)] out TextSlice? textElement, out error = null; return true; } - else if ('\r'.EqualsSpans(span) - && '\n'.EqualsSpans(_reader.PeekCharSpan(1))) + else if ('\r' == c + && '\n' == _reader.PeekChar(1)) { _reader.Position += 1; // This takes one less element because it converts CRLF endings @@ -635,7 +640,7 @@ private bool TryGetTextSlice([NotNullWhen(true)] out TextSlice? textElement, out error = null; return true; } - else if ('{'.EqualsSpans(span)) + else if ('{' == c) { textElement = new TextSlice( startPos, @@ -646,7 +651,7 @@ private bool TryGetTextSlice([NotNullWhen(true)] out TextSlice? textElement, out error = null; return true; } - else if ('}'.EqualsSpans(span)) + else if ('}' == c) { textElement = null; error = ParseError.UnbalancedClosingBrace(_reader.Position, _reader.Row); @@ -659,6 +664,12 @@ private bool TryGetTextSlice([NotNullWhen(true)] out TextSlice? textElement, out } } + if (_reader.Position < _reader.GetData.Length) + { + textElementType = TextElementType.NonBlank; + _reader.Position = _reader.GetData.Length; + } + textElement = new TextSlice( startPos, _reader.Position, @@ -767,8 +778,8 @@ private bool TryGetExpression([NotNullWhen(true)] out IExpression? retVal, out P _reader.SkipBlank(); - if (!'-'.EqualsSpans(_reader.PeekCharSpan()) - || !'>'.EqualsSpans(_reader.PeekCharSpan(1))) + if ('-' != _reader.PeekChar() + || '>' != _reader.PeekChar(1)) { if (inlineExpression is TermReference { Attribute: { } }) { @@ -906,7 +917,7 @@ private bool TryGetVariantKey([NotNullWhen(true)] out Variant? variantKey, out P VariantType variantType; - if (_reader.PeekCharSpan().IsNumberStart()) + if (_reader.TryPeekChar(out var c) && c.IsNumberStart()) { variantType = VariantType.NumberLiteral; if (!TryGetNumberLiteral(out var num, out error)) @@ -942,182 +953,185 @@ private bool TryGetVariantKey([NotNullWhen(true)] out Variant? variantKey, out P private bool TryGetInlineExpression(bool onlyLiteral, [NotNullWhen(true)] out IInlineExpression? expr, out ParseError? error) { - var peekChr = _reader.PeekCharSpan(); - if ('"'.EqualsSpans(peekChr)) + if (_reader.TryPeekChar(out var peekChr)) { - _reader.Position += 1; - var start = _reader.Position; - while (true) + if ('"' == peekChr) { - var b = _reader.PeekCharSpan(); - if ('\\'.EqualsSpans(b)) + _reader.Position += 1; + var start = _reader.Position; + while (true) { - var c = _reader.PeekCharSpan(1); - if (c.IsOneOf('\\', '{', '"')) - { - _reader.Position += 2; - } - else if ('u'.EqualsSpans(c)) + var b = _reader.PeekChar(); + if ('\\' == b) { - _reader.Position += 2; - if (!TrySkipUnicodeSequence(4, out error)) + if (_reader.TryPeekCharAt(_reader.Position + 1, out var c)) { - expr = null; - return false; + if (c.IsOneOf('\\', '{', '"')) + { + _reader.Position += 2; + } + else if ('u'== c) + { + _reader.Position += 2; + if (!TrySkipUnicodeSequence(4, out error)) + { + expr = null; + return false; + } + } + else if ('U'== c) + { + _reader.Position += 2; + if (!TrySkipUnicodeSequence(6, out error)) + { + expr = null; + return false; + } + } + else + { + error = ParseError.UnknownEscapeSequence(c, _reader.Position, _reader.Row); + expr = null; + return false; + } } } - else if ('U'.EqualsSpans(c)) + else if ('"' == b) { - _reader.Position += 2; - if (!TrySkipUnicodeSequence(6, out error)) - { - expr = null; - return false; - } + break; } - else + else if ('\n' == b) { - var seq = c[0]; - error = ParseError.UnknownEscapeSequence(seq, _reader.Position, _reader.Row); + error = ParseError.UnterminatedStringLiteral(_reader.Position, _reader.Row); expr = null; return false; } + else + { + _reader.Position += 1; + } } - else if ('"'.EqualsSpans(b)) - { - break; - } - else if ('\n'.EqualsSpans(b)) + + if (!TryExpectChar('"', out error)) { - error = ParseError.UnterminatedStringLiteral(_reader.Position, _reader.Row); expr = null; return false; } - else - { - _reader.Position += 1; - } - } - if (!TryExpectChar('"', out error)) - { - expr = null; - return false; + var slice = _reader.ReadSlice(start, _reader.Position - 1); + expr = new TextLiteral(slice); + return true; } - var slice = _reader.ReadSlice(start, _reader.Position - 1); - expr = new TextLiteral(slice); - return true; - } - - if (peekChr.IsAsciiDigit()) - { - if (!TryGetNumberLiteral(out var num, out error)) + if (peekChr.IsAsciiDigit()) { - expr = null; - return false; - } + if (!TryGetNumberLiteral(out var num, out error)) + { + expr = null; + return false; + } - expr = new NumberLiteral(num); - return true; - } + expr = new NumberLiteral(num); + return true; + } - if ('-'.EqualsSpans(peekChr) && !onlyLiteral) - { - _reader.Position += 1; - if (_reader.PeekCharSpan().IsAsciiAlphabetic()) + if ('-' == peekChr && !onlyLiteral) { _reader.Position += 1; - var id = GetUncheckedIdentifier(); - if (!TryGetAttributeAccessor(out var attribute, out error)) + if (_reader.TryPeekChar(out var c) && c.IsAsciiAlphabetic()) { - expr = null; - return false; - } + _reader.Position += 1; + var id = GetUncheckedIdentifier(); + if (!TryGetAttributeAccessor(out var attribute, out error)) + { + expr = null; + return false; + } - if (!TryCallArguments(out var args, out error)) + if (!TryCallArguments(out var args, out error)) + { + expr = null; + return false; + } + + expr = new TermReference(id, attribute, args); + return true; + } + else { + _reader.Position -= 1; + if (TryGetNumberLiteral(out var num, out error)) + { + expr = new NumberLiteral(num); + return true; + } + expr = null; return false; } - - expr = new TermReference(id, attribute, args); - return true; } - else + else if ('$' == peekChr && !onlyLiteral) { - _reader.Position -= 1; - if (TryGetNumberLiteral(out var num, out error)) + _reader.Position += 1; + if (!TryGetIdentifier(out var id, out error)) { - expr = new NumberLiteral(num); - return true; + expr = null; + return false; } - expr = null; - return false; - } - } - else if ('$'.EqualsSpans(peekChr) && !onlyLiteral) - { - _reader.Position += 1; - if (!TryGetIdentifier(out var id, out error)) - { - expr = null; - return false; + expr = new VariableReference(id); + return true; } - - expr = new VariableReference(id); - return true; - } - else if (peekChr.IsAsciiAlphabetic()) - { - _reader.Position += 1; - var id = GetUncheckedIdentifier(); - - if (!TryCallArguments(out var args, out error)) + else if (peekChr.IsAsciiAlphabetic()) { - expr = null; - return false; - } + _reader.Position += 1; + var id = GetUncheckedIdentifier(); - if (args != null) - { - if (!id.Name.IsCallee()) + if (!TryCallArguments(out var args, out error)) { - error = ParseError.ForbiddenCallee(_reader.Position, _reader.Row); expr = null; return false; } - error = null; - expr = new FunctionReference(id, args.Value); - return true; + if (args != null) + { + if (!id.Name.Span.IsCallee()) + { + error = ParseError.ForbiddenCallee(_reader.Position, _reader.Row); + expr = null; + return false; + } + + error = null; + expr = new FunctionReference(id, args.Value); + return true; + } + else + { + if (!TryGetAttributeAccessor(out var attribute, out error)) + { + expr = null; + return false; + } + + expr = new MessageReference(id, attribute); + return true; + } } - else + else if ('{' == peekChr && !onlyLiteral) { - if (!TryGetAttributeAccessor(out var attribute, out error)) + _reader.Position += 1; + if (!TryGetPlaceable(out var exp, out error)) { expr = null; return false; } - expr = new MessageReference(id, attribute); + error = null; + expr = new Placeable(exp); return true; } } - else if ('{'.EqualsSpans(peekChr) && !onlyLiteral) - { - _reader.Position += 1; - if (!TryGetPlaceable(out var exp, out error)) - { - expr = null; - return false; - } - - error = null; - expr = new Placeable(exp); - return true; - } if (onlyLiteral) { @@ -1149,7 +1163,7 @@ private bool TryCallArguments(out CallArguments? args, out ParseError? error) while (_reader.IsNotEof) { - if (_reader.IsCurrentChar(')')) + if (')' == _reader.PeekChar()) { break; } @@ -1161,10 +1175,10 @@ private bool TryCallArguments(out CallArguments? args, out ParseError? error) } _reader.SkipBlank(); - if (!_reader.PeekCharSpan().IsOneOf(',', ')')) + if (!(_reader.TryPeekChar(out var c) && c.IsOneOf(',', ')'))) { args = new CallArguments(positional, nameArgs); - error = ParseError.ExpectedToken(',', ')', _reader.PeekCharSpan(), _reader.Position, _reader.Row); + error = ParseError.ExpectedToken(',', ')', _reader.PeekChar(), _reader.Position, _reader.Row); return false; } @@ -1195,7 +1209,7 @@ private bool TryCallArguments(out CallArguments? args, out ParseError? error) { var id = msgRef.Id; _reader.SkipBlank(); - if (_reader.IsCurrentChar(':')) + if (':' == _reader.PeekChar()) { if (argNames.Contains(id)) { @@ -1280,7 +1294,7 @@ private bool TryGetNumberLiteral(out ReadOnlyMemory num, out ParseError? e private bool TrySkipDigits(out ParseError? error) { var start = _reader.Position; - while (_reader.PeekCharSpan().IsAsciiDigit()) + while (_reader.TryPeekChar(out var c) && c.IsAsciiDigit()) { _reader.Position += 1; } @@ -1300,7 +1314,7 @@ private bool TrySkipUnicodeSequence(int length, out ParseError? error) var start = _reader.Position; for (int i = 0; i < length; i++) { - if (_reader.PeekCharSpan().IsAsciiHexdigit()) + if (_reader.TryPeekChar(out var c) && c.IsAsciiHexdigit()) { _reader.Position += 1; }