From c6b45f42fbe4d5774dbc3a3a91108d507f74206c Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sun, 30 Mar 2025 10:35:10 +0500 Subject: [PATCH] Add comparison of manually exprparser and ours Yoakke obviously produce inefficient parser, since writing manual thing appear to be much more efficient --- .../Parser.Benchmarks/ExpressionBenchmarks.cs | 75 ++-------- .../Parser.Benchmarks/ExpressionParser.cs | 135 ++++++++++++++++++ .../Benchmarks/Parser.Benchmarks/Program.cs | 10 +- .../Properties/launchSettings.json | 11 ++ .../Libraries/Parser/PunctuatedValue.cs | 13 +- .../Tests/Parser.Tests/ParseErrorTests.cs | 85 +++++++++++ 6 files changed, 259 insertions(+), 70 deletions(-) create mode 100644 Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionParser.cs create mode 100644 Sources/SynKit/Benchmarks/Parser.Benchmarks/Properties/launchSettings.json create mode 100644 Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs diff --git a/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionBenchmarks.cs b/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionBenchmarks.cs index 3930f600..d99c45d9 100644 --- a/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionBenchmarks.cs +++ b/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionBenchmarks.cs @@ -15,80 +15,21 @@ public partial class ExpressionBenchmarks private static string source = "(((((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))))+(((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))))))))+((((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))))+(((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))))))))) + (((((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))))+(((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))))))))+((((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))))+(((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))) + ((((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))+(((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))) + ((((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1))) + (((2 + 2) + (1 + 1)) + ((2 + 2) + (1 + 1)))))))))"; [Benchmark] - public ParseResult Parse() + public ParseResult ExpressionParser() { - return new Parser(new Lexer(source)).ParseProgram(); + return new ExpressionParser(new Lexer(source)).ParseProgram(); } + [Benchmark] - public List> Lex() + public ParseResult ManualExpressionParser() { - return new Lexer(source).LexAll(); + return new ManualExpressionParser(new Lexer(source)).ParseProgram(); } -} - -public enum TokenType -{ - [Error] Error, - [End] End, - [Ignore][Regex(Regexes.Whitespace)] Whitespace, - - [Token("(")] OpenParen, - [Token(")")] CloseParen, - - [Token("+")] Add, - [Token("-")] Sub, - [Token("*")] Mul, - [Token("/")] Div, - [Token("%")] Mod, - [Token("^")] Exp, - - [Token(";")] Semicol, - - [Regex(Regexes.IntLiteral)] IntLit, -} - -[Lexer(typeof(TokenType))] -public partial class Lexer -{ - public List> LexAll() + [Benchmark] + public List> Lex() { - var list = new List>(); - while (true) - { - var token = Next(); - list.Add(token); - if (token.Kind == TokenType.End) break; - } - return list; + return new Lexer(source).LexAll(); } } - -[Parser(typeof(TokenType))] -public partial class Parser -{ - [Rule("program: expression ';'")] - public static int Program(int n, IToken _) => n; - - [Right("^")] - [Left("*", "/", "%")] - [Left("+", "-")] - [Rule("expression")] - public static int BinOp(int a, IToken op, int b) => op.Text switch - { - "^" => (int)Math.Pow(a, b), - "*" => a * b, - "/" => a / b, - "%" => a % b, - "+" => a + b, - "-" => a - b, - _ => throw new NotImplementedException(), - }; - - [Rule("expression : '(' expression ')'")] - public static int Grouping(IToken _1, int n, IToken _2) => n; - - [Rule("expression : IntLit")] - public static int IntLit(IToken token) => int.Parse(token.Text); -} diff --git a/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionParser.cs b/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionParser.cs new file mode 100644 index 00000000..8117720d --- /dev/null +++ b/Sources/SynKit/Benchmarks/Parser.Benchmarks/ExpressionParser.cs @@ -0,0 +1,135 @@ +// Copyright (c) 2021-2022 Yoakke. +// Licensed under the Apache License, Version 2.0. +// Source repository: https://github.com/LanguageDev/Yoakke + +using Yoakke.SynKit.Lexer; +using Yoakke.SynKit.Parser; +using Yoakke.SynKit.Lexer.Attributes; +using Yoakke.SynKit.Parser.Attributes; + +namespace Yoakke.SynKit.Parser.Benchmarks; + +public enum TokenType +{ + [Error] Error, + [End] End, + [Ignore][Regex(Regexes.Whitespace)] Whitespace, + + [Token("(")] OpenParen, + [Token(")")] CloseParen, + + [Token("+")] Add, + [Token("-")] Sub, + [Token("*")] Mul, + [Token("/")] Div, + [Token("%")] Mod, + [Token("^")] Exp, + + [Token(";")] Semicol, + + [Regex(Regexes.IntLiteral)] IntLit, +} + +[Lexer(typeof(TokenType))] +public partial class Lexer +{ + public List> LexAll() + { + var list = new List>(); + while (true) + { + var token = Next(); + list.Add(token); + if (token.Kind == TokenType.End) break; + } + return list; + } +} + +[Parser(typeof(TokenType))] +public partial class ExpressionParser +{ + [Rule("program: expression ';'")] + public static int Program(int n, IToken _) => n; + + [Right("^")] + [Left("*", "/", "%")] + [Left("+", "-")] + [Rule("expression")] + public static int BinOp(int a, IToken op, int b) => op.Text switch + { + "^" => (int)Math.Pow(a, b), + "*" => a * b, + "/" => a / b, + "%" => a % b, + "+" => a + b, + "-" => a - b, + _ => throw new NotImplementedException(), + }; + + [Rule("expression : '(' expression ')'")] + public static int Grouping(IToken _1, int n, IToken _2) => n; + + [Rule("expression : IntLit")] + public static int IntLit(IToken token) => int.Parse(token.Text); +} + +[Parser(typeof(TokenType))] +public partial class ManualExpressionParser +{ + [Rule("program: expression ';'")] + public static int Program(int n, IToken _) => n; + + [Rule("expression_level1_operator: ('+' | '-')")] + [Rule("expression_level2_operator: ('*' | '/' | '%')")] + [Rule("expression_level3_operator: ('^')")] + public static IToken Level1Operator(IToken op) => op; + + [Rule("expression: (expression_level1 (expression_level1_operator expression_level1)*)")] + [Rule("expression_level1: (expression_level2 (expression_level2_operator expression_level2)*)")] + [Rule("expression_level2: (expression_atomic (expression_level3_operator expression_atomic)*)")] + public static int Pop(Punctuated p) + { + int result = 0; + IToken? lastOp = null; + foreach (var (n, op) in p) + { + if (lastOp is null) + { + result = n; + lastOp = op; + } + else + { + if (op is null) + { + result = BinOp(result, lastOp, n); + } + else + { + result = BinOp(result, lastOp, n); + lastOp = op; + } + } + } + + return result; + } + + public static int BinOp(int a, IToken op, int b) => op.Text switch + { + "^" => (int)Math.Pow(a, b), + "*" => a * b, + "/" => a / b, + "%" => a % b, + "+" => a + b, + "-" => a - b, + _ => throw new NotImplementedException(), + }; + + [Rule("expression_atomic : '(' expression ')'")] + public static int Grouping(IToken _1, int n, IToken _2) => n; + + [Rule("expression_atomic : IntLit")] + public static int IntLit(IToken token) => int.Parse(token.Text); +} diff --git a/Sources/SynKit/Benchmarks/Parser.Benchmarks/Program.cs b/Sources/SynKit/Benchmarks/Parser.Benchmarks/Program.cs index b9a3713a..e3924678 100644 --- a/Sources/SynKit/Benchmarks/Parser.Benchmarks/Program.cs +++ b/Sources/SynKit/Benchmarks/Parser.Benchmarks/Program.cs @@ -4,5 +4,11 @@ using BenchmarkDotNet.Running; -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - +if (args.Length == 1 && args[0] == "parser") +{ + new Yoakke.SynKit.Parser.Benchmarks.ExpressionBenchmarks().ManualExpressionParser(); +} +else +{ + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); +} diff --git a/Sources/SynKit/Benchmarks/Parser.Benchmarks/Properties/launchSettings.json b/Sources/SynKit/Benchmarks/Parser.Benchmarks/Properties/launchSettings.json new file mode 100644 index 00000000..bd7d20b1 --- /dev/null +++ b/Sources/SynKit/Benchmarks/Parser.Benchmarks/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Parser.Benchmarks": { + "commandName": "Project" + }, + "Parser": { + "commandName": "Project", + "commandLineArgs": "parser" + } + } +} \ No newline at end of file diff --git a/Sources/SynKit/Libraries/Parser/PunctuatedValue.cs b/Sources/SynKit/Libraries/Parser/PunctuatedValue.cs index 15919533..7fa49bc7 100644 --- a/Sources/SynKit/Libraries/Parser/PunctuatedValue.cs +++ b/Sources/SynKit/Libraries/Parser/PunctuatedValue.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2022 Yoakke. +// Copyright (c) 2021-2022 Yoakke. // Licensed under the Apache License, Version 2.0. // Source repository: https://github.com/LanguageDev/Yoakke @@ -31,4 +31,15 @@ public PunctuatedValue(TValue element, TPunct? punctuation) this.Value = element; this.Punctuation = punctuation; } + + /// + /// Deconstructs an object into its value and optional punctuation components. + /// + /// Represents the main value extracted from the object. + /// Represents an optional punctuation element associated with the value. + public void Deconstruct(out TValue value, out TPunct? punct) + { + value = this.Value; + punct = this.Punctuation!; + } } diff --git a/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs b/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs new file mode 100644 index 00000000..52a5ca19 --- /dev/null +++ b/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2021-2022 Yoakke. +// Licensed under the Apache License, Version 2.0. +// Source repository: https://github.com/LanguageDev/Yoakke + +using Xunit; + +namespace Yoakke.SynKit.Parser.Tests; + +public class ParseErrorTests +{ + [Fact] + public void MergeSameError() + { + var firstError = new ParseError("^", null, 12, "expression"); + var secondError = new ParseError("^", null, 12, "expression"); + + var result = firstError | secondError; + + Assert.NotNull(result); + Assert.Null(result.Got); + Assert.Equal(12, result.Position); + Assert.Equal(1, result.Elements.Count); + Assert.True(result.Elements.ContainsKey("expression")); + Assert.Equal(1, result.Elements["expression"].Expected.Count); + Assert.True(result.Elements["expression"].Expected.Contains("^")); + } + + [Fact] + public void MergeSameErrorDifferentExpectations() + { + var firstError = new ParseError("^", null, 12, "expression"); + var secondError = new ParseError("|", null, 12, "expression"); + + var result = firstError | secondError; + + Assert.NotNull(result); + Assert.Null(result.Got); + Assert.Equal(12, result.Position); + Assert.Equal(1, result.Elements.Count); + Assert.True(result.Elements.ContainsKey("expression")); + Assert.Equal("expression", result.Elements["expression"].Context); + Assert.Equal(2, result.Elements["expression"].Expected.Count); + Assert.True(result.Elements["expression"].Expected.Contains("^")); + Assert.True(result.Elements["expression"].Expected.Contains("|")); + } + + [Fact] + public void MergeThreeErrors() + { + var firstError = new ParseError("^", null, 12, "expression"); + var secondError = new ParseError("|", null, 12, "expression"); + var thirdError = new ParseError(":", null, 12, "expression"); + + var result = firstError | secondError | thirdError; + + Assert.NotNull(result); + Assert.Null(result.Got); + Assert.Equal(12, result.Position); + Assert.Equal(1, result.Elements.Count); + Assert.True(result.Elements.ContainsKey("expression")); + Assert.Equal(3, result.Elements["expression"].Expected.Count); + Assert.True(result.Elements["expression"].Expected.Contains("^")); + Assert.True(result.Elements["expression"].Expected.Contains("|")); + Assert.True(result.Elements["expression"].Expected.Contains(":")); + } + + [Fact] + public void MergeTwoErrorsFromDifferentExpressions() + { + var firstError = new ParseError("^", null, 12, "expression"); + var secondError = new ParseError("^", null, 12, "other_expression"); + + var result = firstError | secondError; + + Assert.NotNull(result); + Assert.Null(result.Got); + Assert.Equal(12, result.Position); + Assert.Equal(2, result.Elements.Count); + Assert.True(result.Elements.ContainsKey("expression")); + Assert.Equal(1, result.Elements["expression"].Expected.Count); + Assert.True(result.Elements["expression"].Expected.Contains("^")); + Assert.Equal(1, result.Elements["other_expression"].Expected.Count); + Assert.True(result.Elements["other_expression"].Expected.Contains("^")); + } +}