From 841efbcfcee25a84c9a42696368fd53a7ab93a41 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 9 Jun 2018 18:46:16 -0500 Subject: [PATCH] Make Reader a ref struct and have it operate directly on ReadOnlySpans to use less memory. --- .../Markup/Parsers/ArgumentListParser.cs | 4 +- .../Parsers/ExpressionObserverBuilder.cs | 4 +- .../Markup/Parsers/ExpressionParser.cs | 48 ++++----- .../Markup/Parsers/IdentifierParser.cs | 47 +++++++++ .../Avalonia.Markup/Markup/Parsers/Reader.cs | 82 ++++++---------- .../Markup/Parsers/SelectorGrammar.cs | 98 ++++++++++--------- 6 files changed, 157 insertions(+), 126 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index d3f850fbf6..6a0bde6f83 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Parsers { internal static class ArgumentListParser { - public static IList Parse(Reader r, char open, char close) + public static IList ParseArguments(this ref Reader r, char open, char close) { if (r.Peek == open) { @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Parsers while (!r.End) { - var argument = r.TakeUntil(','); + var argument = r.TakeWhile(c => c != ',' && c != close); if (argument.IsEmpty) { throw new ExpressionParseException(r.Position, "Expected indexer argument."); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 7141a62cd9..b115fc0cdf 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -15,9 +15,9 @@ namespace Avalonia.Markup.Parsers return new EmptyExpressionNode(); } - var reader = new Reader(expression); + var reader = new Reader(expression.AsSpan()); var parser = new ExpressionParser(enableValidation, typeResolver); - var node = parser.Parse(reader); + var node = parser.Parse(ref reader); if (!reader.End) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 9ac70df56b..bb22046053 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Parsers _enableValidation = enableValidation; } - public ExpressionNode Parse(Reader r) + public ExpressionNode Parse(ref Reader r) { var nodes = new List(); var state = State.Start; @@ -30,23 +30,23 @@ namespace Avalonia.Markup.Parsers switch (state) { case State.Start: - state = ParseStart(r, nodes); + state = ParseStart(ref r, nodes); break; case State.AfterMember: - state = ParseAfterMember(r, nodes); + state = ParseAfterMember(ref r, nodes); break; case State.BeforeMember: - state = ParseBeforeMember(r, nodes); + state = ParseBeforeMember(ref r, nodes); break; case State.AttachedProperty: - state = ParseAttachedProperty(r, nodes); + state = ParseAttachedProperty(ref r, nodes); break; case State.Indexer: - state = ParseIndexer(r, nodes); + state = ParseIndexer(ref r, nodes); break; } } @@ -64,18 +64,18 @@ namespace Avalonia.Markup.Parsers return nodes.FirstOrDefault(); } - private State ParseStart(Reader r, IList nodes) + private State ParseStart(ref Reader r, IList nodes) { - if (ParseNot(r)) + if (ParseNot(ref r)) { nodes.Add(new LogicalNotNode()); return State.Start; } - else if (ParseOpenBrace(r)) + else if (ParseOpenBrace(ref r)) { return State.AttachedProperty; } - else if (PeekOpenBracket(r)) + else if (PeekOpenBracket(ref r)) { return State.Indexer; } @@ -93,18 +93,18 @@ namespace Avalonia.Markup.Parsers return State.End; } - private static State ParseAfterMember(Reader r, IList nodes) + private static State ParseAfterMember(ref Reader r, IList nodes) { - if (ParseMemberAccessor(r)) + if (ParseMemberAccessor(ref r)) { return State.BeforeMember; } - else if (ParseStreamOperator(r)) + else if (ParseStreamOperator(ref r)) { nodes.Add(new StreamNode()); return State.AfterMember; } - else if (PeekOpenBracket(r)) + else if (PeekOpenBracket(ref r)) { return State.Indexer; } @@ -112,9 +112,9 @@ namespace Avalonia.Markup.Parsers return State.End; } - private State ParseBeforeMember(Reader r, IList nodes) + private State ParseBeforeMember(ref Reader r, IList nodes) { - if (ParseOpenBrace(r)) + if (ParseOpenBrace(ref r)) { return State.AttachedProperty; } @@ -132,7 +132,7 @@ namespace Avalonia.Markup.Parsers } } - private State ParseAttachedProperty(Reader r, List nodes) + private State ParseAttachedProperty(ref Reader r, List nodes) { ReadOnlySpan ns = ReadOnlySpan.Empty; ReadOnlySpan owner; @@ -166,9 +166,9 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private State ParseIndexer(Reader r, List nodes) + private State ParseIndexer(ref Reader r, List nodes) { - var args = ArgumentListParser.Parse(r, '[', ']'); + var args = r.ParseArguments('[', ']'); if (args.Count == 0) { @@ -179,27 +179,27 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static bool ParseNot(Reader r) + private static bool ParseNot(ref Reader r) { return !r.End && r.TakeIf('!'); } - private static bool ParseMemberAccessor(Reader r) + private static bool ParseMemberAccessor(ref Reader r) { return !r.End && r.TakeIf('.'); } - private static bool ParseOpenBrace(Reader r) + private static bool ParseOpenBrace(ref Reader r) { return !r.End && r.TakeIf('('); } - private static bool PeekOpenBracket(Reader r) + private static bool PeekOpenBracket(ref Reader r) { return !r.End && r.Peek == '['; } - private static bool ParseStreamOperator(Reader r) + private static bool ParseStreamOperator(ref Reader r) { return !r.End && r.TakeIf('^'); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs new file mode 100644 index 0000000000..a2f6c97608 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -0,0 +1,47 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Avalonia.Markup.Parsers +{ + internal static class IdentifierParser + { + public static ReadOnlySpan ParseIdentifier(this ref Reader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + return r.TakeWhile(IsValidIdentifierChar); + } + else + { + return ReadOnlySpan.Empty; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c)) + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 82a02b10ab..dae05eedf9 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -7,27 +7,32 @@ using System.Text; namespace Avalonia.Markup.Parsers { - internal class Reader + internal ref struct Reader { - private readonly string _s; - private int _i; + private ReadOnlySpan _s; - public Reader(string s) + public Reader(ReadOnlySpan s) + :this() { _s = s; } - public bool End => _i == _s.Length; - public char Peek => _s[_i]; - public int Position => _i; - public char Take() => _s[_i++]; + public bool End => _s.IsEmpty; + public char Peek => _s[0]; + public int Position { get; private set; } + public char Take() + { + Position++; + char taken = _s[0]; + _s = _s.Slice(1); + return taken; + } public void SkipWhitespace() { - while (!End && char.IsWhiteSpace(Peek)) - { - Take(); - } + var trimmed = _s.TrimStart(); + Position += _s.Length - trimmed.Length; + _s = trimmed; } public bool TakeIf(char c) @@ -55,53 +60,26 @@ namespace Avalonia.Markup.Parsers public ReadOnlySpan TakeUntil(char c) { - int startIndex = Position; - while (!End && Peek != c) + int len; + for (len = 0; len < _s.Length && _s[len] != c; len++) { - Take(); } - return _s.AsSpan(startIndex, Position - startIndex); + var span = _s.Slice(0, len); + _s = _s.Slice(len); + Position += len; + return span; } - public ReadOnlySpan ParseIdentifier() + public ReadOnlySpan TakeWhile(Func condition) { - if (IsValidIdentifierStart(Peek)) - { - int startIndex = Position; - - while (!End && IsValidIdentifierChar(Peek)) - { - Take(); - } - - return _s.AsSpan(startIndex, Position - startIndex); - } - else - { - return ReadOnlySpan.Empty; - } - } - - private static bool IsValidIdentifierStart(char c) - { - return char.IsLetter(c) || c == '_'; - } - - private static bool IsValidIdentifierChar(char c) - { - if (IsValidIdentifierStart(c)) - { - return true; - } - else + int len; + for (len = 0; len < _s.Length && condition(_s[len]); len++) { - var cat = CharUnicodeInfo.GetUnicodeCategory(c); - return cat == UnicodeCategory.NonSpacingMark || - cat == UnicodeCategory.SpacingCombiningMark || - cat == UnicodeCategory.ConnectorPunctuation || - cat == UnicodeCategory.Format || - cat == UnicodeCategory.DecimalDigitNumber; } + var span = _s.Slice(0, len); + _s = _s.Slice(len); + Position += len; + return span; } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 02123b11f2..d00f28a85a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -31,7 +31,7 @@ namespace Avalonia.Markup.Parsers public static IEnumerable Parse(string s) { - var r = new Reader(s); + var r = new Reader(s.AsSpan()); var state = State.Start; var selector = new List(); while (!r.End && state != State.End) @@ -40,34 +40,34 @@ namespace Avalonia.Markup.Parsers switch (state) { case State.Start: - (state, syntax) = ParseStart(r); + state = ParseStart(ref r); break; case State.Middle: - (state, syntax) = ParseMiddle(r); + state = ParseMiddle(ref r); + break; + case State.CanHaveType: + state = ParseCanHaveType(ref r); break; case State.Colon: - (state, syntax) = ParseColon(r); + (state, syntax) = ParseColon(ref r); break; case State.Class: - (state, syntax) = ParseClass(r); + (state, syntax) = ParseClass(ref r); break; case State.Traversal: - (state, syntax) = ParseTraversal(r); + (state, syntax) = ParseTraversal(ref r); break; case State.TypeName: - (state, syntax) = ParseTypeName(r); - break; - case State.CanHaveType: - (state, syntax) = ParseCanHaveType(r); + (state, syntax) = ParseTypeName(ref r); break; case State.Property: - (state, syntax) = ParseProperty(r); + (state, syntax) = ParseProperty(ref r); break; case State.Template: - (state, syntax) = ParseTemplate(r); + (state, syntax) = ParseTemplate(ref r); break; case State.Name: - (state, syntax) = ParseName(r); + (state, syntax) = ParseName(ref r); break; } if (syntax != null) @@ -84,59 +84,64 @@ namespace Avalonia.Markup.Parsers return selector; } - private static (State, ISyntax) ParseStart(Reader r) + private static State ParseStart(ref Reader r) { r.SkipWhitespace(); + if (r.End) + { + return State.End; + } + if (r.TakeIf(':')) { - return (State.Colon, null); + return State.Colon; } else if (r.TakeIf('.')) { - return (State.Class, null); + return State.Class; } else if (r.TakeIf('#')) { - return (State.Name, null); + return State.Name; } - return (State.TypeName, null); + return State.TypeName; } - private static (State, ISyntax) ParseMiddle(Reader r) + private static State ParseMiddle(ref Reader r) { if (r.TakeIf(':')) { - return (State.Colon, null); + return State.Colon; } else if (r.TakeIf('.')) { - return (State.Class, null); + return State.Class; } else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>') { - return (State.Traversal, null); + return State.Traversal; } else if (r.TakeIf('/')) { - return (State.Template, null); + return State.Template; } else if (r.TakeIf('#')) { - return (State.Name, null); + return State.Name; } - return (State.TypeName, null); + return State.TypeName; } - private static (State, ISyntax) ParseCanHaveType(Reader r) + private static State ParseCanHaveType(ref Reader r) { if (r.TakeIf('[')) { - return (State.Property, null); + return State.Property; } - return (State.Middle, null); + return State.Middle; } - private static (State, ISyntax) ParseColon(Reader r) + private static (State, ISyntax) ParseColon(ref Reader r) { var identifier = r.ParseIdentifier(); @@ -145,9 +150,10 @@ namespace Avalonia.Markup.Parsers throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'."); } - if (identifier.SequenceEqual("is".AsSpan()) && r.TakeIf('(')) + const string IsKeyword = "is"; + if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { - var syntax = ParseType(r); + var syntax = ParseType(ref r, new IsSyntax()); if (r.End || !r.TakeIf(')')) { throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}"); @@ -166,7 +172,7 @@ namespace Avalonia.Markup.Parsers } } - private static (State, ISyntax) ParseTraversal(Reader r) + private static (State, ISyntax) ParseTraversal(ref Reader r) { r.SkipWhitespace(); if (r.TakeIf('>')) @@ -188,7 +194,7 @@ namespace Avalonia.Markup.Parsers } } - private static (State, ISyntax) ParseClass(Reader r) + private static (State, ISyntax) ParseClass(ref Reader r) { var @class = r.ParseIdentifier(); if (@class.IsEmpty) @@ -199,10 +205,11 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new ClassSyntax { Class = @class.ToString() }); } - private static (State, ISyntax) ParseTemplate(Reader r) + private static (State, ISyntax) ParseTemplate(ref Reader r) { var template = r.ParseIdentifier(); - if (!template.SequenceEqual(nameof(template).AsSpan())) + const string TemplateKeyword = "template"; + if (!template.SequenceEqual(TemplateKeyword.AsSpan())) { throw new ExpressionParseException(r.Position, $"Expected 'template', got '{template.ToString()}'"); } @@ -213,7 +220,7 @@ namespace Avalonia.Markup.Parsers return (State.Start, new TemplateSyntax()); } - private static (State, ISyntax) ParseName(Reader r) + private static (State, ISyntax) ParseName(ref Reader r) { var name = r.ParseIdentifier(); if (name.IsEmpty) @@ -223,12 +230,12 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new NameSyntax { Name = name.ToString() }); } - private static (State, ISyntax) ParseTypeName(Reader r) + private static (State, ISyntax) ParseTypeName(ref Reader r) { - return (State.CanHaveType, ParseType(r)); + return (State.CanHaveType, ParseType(ref r, new OfTypeSyntax())); } - private static (State, ISyntax) ParseProperty(Reader r) + private static (State, ISyntax) ParseProperty(ref Reader r) { var property = r.ParseIdentifier(); @@ -244,8 +251,8 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new PropertySyntax { Property = property.ToString(), Value = value.ToString() }); } - private static TSyntax ParseType(Reader r) - where TSyntax : ITypeSyntax, new() + private static TSyntax ParseType(ref Reader r, TSyntax syntax) + where TSyntax : ITypeSyntax { ReadOnlySpan ns = null; ReadOnlySpan type; @@ -269,11 +276,10 @@ namespace Avalonia.Markup.Parsers { type = namespaceOrTypeName; } - return new TSyntax - { - Xmlns = ns.ToString(), - TypeName = type.ToString() - }; + + syntax.Xmlns = ns.ToString(); + syntax.TypeName = type.ToString(); + return syntax; } public interface ISyntax