diff --git a/src/RCParsing/Building/ParserRules/BuildableSequenceParserRule.cs b/src/RCParsing/Building/ParserRules/BuildableSequenceParserRule.cs index 52a5738..8a1749d 100644 --- a/src/RCParsing/Building/ParserRules/BuildableSequenceParserRule.cs +++ b/src/RCParsing/Building/ParserRules/BuildableSequenceParserRule.cs @@ -16,6 +16,12 @@ public class BuildableSequenceParserRule : BuildableParserRule /// The elements of the sequence parser rule. /// public List> Elements { get; } = new List>(); + + /// + /// Labels for the elements of the sequence parser rule. + /// + public Dictionary Labels { get; set; } = new Dictionary(); + public override IEnumerable>? RuleChildren => Elements; public override IEnumerable>? TokenChildren => null; @@ -26,20 +32,22 @@ public BuildableSequenceParserRule() protected override ParserRule BuildRule(List? ruleChildren, List? tokenChildren) { - return new SequenceParserRule(ruleChildren); + return new SequenceParserRule(ruleChildren, Labels); } public override bool Equals(object? obj) { return base.Equals(obj) && obj is BuildableSequenceParserRule other && - Elements.SequenceEqual(other.Elements); + Elements.SequenceEqual(other.Elements) && + Labels.SequenceEqual(other.Labels); } public override int GetHashCode() { int hashCode = base.GetHashCode(); - hashCode = hashCode * 397 + Elements.GetSequenceHashCode() * 23; + hashCode = hashCode * 397 + Elements.GetSequenceHashCode() * 17; + hashCode = hashCode * 397 + Labels.GetSequenceHashCode() * 17; return hashCode; } } diff --git a/src/RCParsing/Building/RuleBuilder.cs b/src/RCParsing/Building/RuleBuilder.cs index 626f5b8..d686a4f 100644 --- a/src/RCParsing/Building/RuleBuilder.cs +++ b/src/RCParsing/Building/RuleBuilder.cs @@ -100,6 +100,37 @@ public RuleBuilder Rule(Or childRule) return this; } + /// + /// Sets a label for last rule in the sequence. Converts current rule to sequence if it is not already one. + /// + /// Label to assign to the last rule in the sequence. + /// Current instance for method chaining. + public RuleBuilder Label(string label) + { + if (!BuildingRule.HasValue) + { + throw new ParserBuildingException("Cannot label an empty sequence."); + } + else if (BuildingRule.Value.VariantIndex == 1 && + BuildingRule.Value.AsT2() is BuildableSequenceParserRule sequenceRule) + { + int lastIndex = sequenceRule.Elements.Count - 1; + if (sequenceRule.Labels.ContainsValue(lastIndex)) + sequenceRule.Labels.Remove(sequenceRule.Labels.First(v => v.Value == lastIndex).Key); + if (sequenceRule.Labels.ContainsKey(label)) + throw new ParserBuildingException("Label already exists in this sequence element."); + sequenceRule.Labels[label] = lastIndex; + } + else + { + var newSequence = new BuildableSequenceParserRule(); + newSequence.Elements.Add(BuildingRule.Value); + newSequence.Labels[label] = 0; + BuildingRule = newSequence; + } + return this; + } + /// /// Adds a rule to the current sequence. /// diff --git a/src/RCParsing/ParsedRuleResult.cs b/src/RCParsing/ParsedRuleResult.cs index bd6fb13..b2658de 100644 --- a/src/RCParsing/ParsedRuleResult.cs +++ b/src/RCParsing/ParsedRuleResult.cs @@ -11,7 +11,7 @@ namespace RCParsing /// public sealed class ParsedRuleResult : ParsedRuleResultBase { - public override ParsedRuleResultBase this[int index] => + public override ParsedRuleResultBase GetChild(int index) => new ParsedRuleResult(this, ContextReference, Result.children[index]); public override int Count => Result.children?.Count ?? 0; public override IEnumerator GetEnumerator() @@ -77,8 +77,8 @@ public sealed class ParsedRuleResultOptimized : ParsedRuleResultBase, IOptimized { public ParseTreeOptimization Optimization { get; } - public override ParsedRuleResultBase this[int index] => - new ParsedRuleResultOptimized(Optimization, this, ContextReference, Result.children[index]); + public override ParsedRuleResultBase GetChild(int index) + => new ParsedRuleResultOptimized(Optimization, this, ContextReference, Result.children[index]); public override int Count => Result.children?.Count ?? 0; public override IEnumerator GetEnumerator() { diff --git a/src/RCParsing/ParsedRuleResultBase.cs b/src/RCParsing/ParsedRuleResultBase.cs index 94aced6..fcc94c0 100644 --- a/src/RCParsing/ParsedRuleResultBase.cs +++ b/src/RCParsing/ParsedRuleResultBase.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Xml.Linq; using System.Xml.Schema; @@ -168,7 +169,15 @@ public virtual ParsedTokenResult? Token /// /// The zero-based index of the child AST node to get. /// The child AST node at the specified index. - public virtual ParsedRuleResultBase this[int index] => Children[index]; + public ParsedRuleResultBase this[int index] => GetChild(index); + + /// + /// Gets the child AST node with the specified label inside the sequence rule. + /// Throws an exception if no such child exists or this AST node is not belongs to sequence rule. + /// + /// The label of the child AST node to get. + /// The child AST node with the specified label. + public ParsedRuleResultBase this[string label] => GetChild(label); public virtual IEnumerator GetEnumerator() { @@ -187,11 +196,78 @@ IEnumerator IEnumerable.GetEnumerator() /// A new parsed result with updated context and parsed rule. public abstract ParsedRuleResultBase Updated(ParserContext newContext, ParsedRule newParsedRule); + /// + /// Gets the child AST node at the specified index. Throws an exception if the index is out of range. + /// + /// The zero-based index of the child AST node to get. + /// The child AST node at the specified index. + public virtual ParsedRuleResultBase GetChild(int index) + { + return Children[index]; + } + + /// + /// Gets the child AST node at the specified index. Returns null if the index is out of range. + /// + /// The zero-based index of the child AST node to get. + /// The child AST node at the specified index. + public virtual ParsedRuleResultBase? TryGetChild(int index) + { + if (index >= 0 && index < Children.Count) + return Children[index]; + return null; + } + + /// + /// Gets the child AST node with the specified label inside the sequence rule. + /// Throws an exception if no such child exists or this AST node is not belongs to sequence rule. + /// + /// The label of the child AST node to get. + /// The child AST node with the specified label. + public ParsedRuleResultBase GetChild(string label) + { + if (Rule is SequenceParserRule sequence) + { + if (sequence.RuleLabels.TryGetValue(label, out var index)) + { + return GetChild(index); + } + throw new SemanticException(this, $"Cannot find child with label '{label}' in sequence rule '{Rule.ToString(0)}'."); + } + throw new SemanticException(this, $"Cannot get child with label '{label}' from non-sequence rule '{Rule.ToString(0)}'."); + } + + /// + /// Gets the child AST node with the specified label inside the sequence rule. + /// Returns null if no such child exists or this AST node is not belongs to sequence rule. + /// + /// The label of the child AST node to get. + /// The child AST node with the specified label. + public ParsedRuleResultBase? TryGetChild(string label) + { + if (Rule is SequenceParserRule sequence) + { + if (sequence.RuleLabels.TryGetValue(label, out var index)) + { + return TryGetChild(index); + } + } + return null; + } + /// /// Gets the text captured by child AST node at the specific index. /// /// The text captured by child AST node. - public string GetText(int index) => this[index].Text; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetText(int index) => GetChild(index).Text; + + /// + /// Gets the text captured by child AST node marked by specific label in sequence rule. + /// + /// The text captured by child AST node. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetText(string label) => GetChild(label).Text; /// /// Gets the intermediate value associated with this AST node as an instance of type . @@ -206,8 +282,26 @@ public T GetIntermediateValue() => IntermediateValue is T res ? res : /// /// The type of value to retrieve. /// The intermediate value associated with child AST node. - public T GetIntermediateValue(int index) => this[index].IntermediateValue is T res ? res : - throw new SemanticException(this[index], $"Expected an intermediate value of type {typeof(T).Name} but got {IntermediateValue?.GetType().Name ?? "null"}."); + public T GetIntermediateValue(int index) + { + var child = GetChild(index); + return child.IntermediateValue is T res ? res : + throw new SemanticException(child, + $"Expected an intermediate value of type {typeof(T).Name} but got {IntermediateValue?.GetType().Name ?? "null"}."); + } + + /// + /// Gets the intermediate value associated with child AST node marked with specified label as an instance of type . + /// + /// The type of value to retrieve. + /// The intermediate value associated with child AST node. + public T GetIntermediateValue(string label) + { + var child = GetChild(label); + return child.IntermediateValue is T res ? res : + throw new SemanticException(child, + $"Expected an intermediate value of type {typeof(T).Name} but got {IntermediateValue?.GetType().Name ?? "null"}."); + } /// /// Tries to get the intermediate value associated with this AST node as an instance of type . @@ -222,7 +316,15 @@ public T GetIntermediateValue(int index) => this[index].IntermediateValue is /// The type of value to retrieve. /// The intermediate value associated with child AST node. public T? TryGetIntermediateValue(int index) - => Count > index ? this[index].IntermediateValue is T result ? result : default : default; + => TryGetChild(index).IntermediateValue is T result ? result : default; + + /// + /// Tries to get the intermediate value associated with child AST node at the specific index as an instance of type . + /// + /// The type of value to retrieve. + /// The intermediate value associated with child AST node. + public T? TryGetIntermediateValue(string label) + => TryGetChild(label).IntermediateValue is T result ? result : default; /// /// Gets the intermediate value associated with this AST node converted to type . @@ -248,13 +350,32 @@ public T ConvertIntermediateValue() /// The intermediate value associated with child AST node. public T ConvertIntermediateValue(int index) { + var child = GetChild(index); try { - return (T)Convert.ChangeType(this[index].IntermediateValue, typeof(T)); + return (T)Convert.ChangeType(child.IntermediateValue, typeof(T)); } catch (Exception ex) { - throw new SemanticException(this[index], $"Failed to convert intermediate value to {typeof(T).Name}: {ex.Message}", ex); + throw new SemanticException(child, $"Failed to convert intermediate value to {typeof(T).Name}: {ex.Message}", ex); + } + } + + /// + /// Gets the intermediate value associated with child AST node marked with specified label converted to type . + /// + /// The type of value to retrieve. + /// The intermediate value associated with child AST node. + public T ConvertIntermediateValue(string label) + { + var child = GetChild(label); + try + { + return (T)Convert.ChangeType(child.IntermediateValue, typeof(T)); + } + catch (Exception ex) + { + throw new SemanticException(child, $"Failed to convert intermediate value to {typeof(T).Name}: {ex.Message}", ex); } } @@ -268,7 +389,21 @@ public T ConvertIntermediateValue(int index) /// Gets the value associated with child AST node at the specific index as not-null object. If the value is null, throws an exception. /// /// The value associated with child AST node. - public object GetValue(int index) => this[index].Value ?? throw new SemanticException(this[index], "ParsedRuleResult[index].Value is null"); + public object GetValue(int index) + { + var child = GetChild(index); + return child.Value ?? throw new SemanticException(child, "ParsedRuleResult[index].Value is null"); + } + + /// + /// Gets the value associated with child AST node at the specific index as not-null object. If the value is null, throws an exception. + /// + /// The value associated with child AST node. + public object GetValue(string label) + { + var child = GetChild(label); + return child.Value ?? throw new SemanticException(child, "ParsedRuleResult[label].Value is null"); + } /// /// Gets the value associated with this AST node as an instance of type . @@ -290,9 +425,24 @@ public T GetValue() /// The value associated with child AST node. public T GetValue(int index) { - var value = this[index].Value; + var child = GetChild(index); + var value = child.Value; + return value is T res ? res : + throw new SemanticException(child, + $"Expected a value of type {typeof(T).Name} but got {value?.GetType().Name ?? "null"}."); + } + + /// + /// Gets the value associated with child AST node marked with specified label as an instance of type . + /// + /// The type of value to retrieve. + /// The value associated with child AST node. + public T GetValue(string label) + { + var child = GetChild(label); + var value = child.Value; return value is T res ? res : - throw new SemanticException(this[index], + throw new SemanticException(child, $"Expected a value of type {typeof(T).Name} but got {value?.GetType().Name ?? "null"}."); } @@ -310,7 +460,15 @@ public T GetValue(int index) /// The type of value to retrieve. /// The value associated with child AST node. public T? TryGetValue(int index) - => Count > index ? this[index].Value is T result ? result : default : default; + => TryGetChild(index).Value is T result ? result : default; + + /// + /// Tries to get the value associated with child AST node marked with specified label as an instance of type or value. + /// + /// The type of value to retrieve. + /// The value associated with child AST node. + public T? TryGetValue(string label) + => TryGetChild(label).Value is T result ? result : default; /// /// Tries to get the value associated with this AST node as an instance of type or value. @@ -326,7 +484,15 @@ public T GetValue(int index) /// The type of value to retrieve. /// The value associated with child AST node. public T? TryGetNullableValue(int index) where T : struct - => Count > index ? this[index].Value is T result ? result : null : null; + => TryGetChild(index).Value is T result ? result : null; + + /// + /// Tries to get the value associated with child AST node marked with specified label as an instance of type or value. + /// + /// The type of value to retrieve. + /// The value associated with child AST node. + public T? TryGetNullableValue(string label) where T : struct + => TryGetChild(label).Value is T result ? result : null; /// /// Gets the value associated with this AST node converted to type . @@ -358,13 +524,35 @@ public T ConvertValue() /// The value associated with child AST node. public T ConvertValue(int index) { + var child = GetChild(index); + try + { + return (T)Convert.ChangeType(child.Value, typeof(T)); + } + catch (Exception ex) + { + throw new SemanticException(child, $"Failed to convert value to {typeof(T).Name}: {ex.Message}", ex); + } + } + + /// + /// Gets the value associated with child AST node marked with specified label converted to type . + /// + /// + /// Value is converted via . + /// + /// The type of value to retrieve. + /// The value associated with child AST node. + public T ConvertValue(string label) + { + var child = GetChild(label); try { - return (T)Convert.ChangeType(this[index].Value, typeof(T)); + return (T)Convert.ChangeType(child.Value, typeof(T)); } catch (Exception ex) { - throw new SemanticException(this[index], $"Failed to convert value to {typeof(T).Name}: {ex.Message}", ex); + throw new SemanticException(child, $"Failed to convert value to {typeof(T).Name}: {ex.Message}", ex); } } @@ -405,7 +593,16 @@ public T GetParsingParameter() => ParsingParameter is T res ? res : /// The values from the children. public object?[] SelectArray(int index) { - return this[index].SelectArray(); + return GetChild(index).SelectArray(); + } + + /// + /// Selects the children values array of child AST node marked with specified label. + /// + /// The values from the children. + public object?[] SelectArray(string label) + { + return GetChild(label).SelectArray(); } /// @@ -423,7 +620,16 @@ public IEnumerable SelectValues() /// The values from the children. public IEnumerable SelectValues(int index) { - return this[index].SelectValues(); + return GetChild(index).SelectValues(); + } + + /// + /// Selects the children values of child AST node marked with specified label. + /// + /// The values from the children. + public IEnumerable SelectValues(string label) + { + return GetChild(label).SelectValues(); } /// @@ -450,7 +656,17 @@ public T[] SelectArray() /// The casted values from the children. public T[] SelectArray(int index) { - return this[index].SelectArray(); + return GetChild(index).SelectArray(); + } + + /// + /// Selects the casted children values array of child AST node marked with specified label. + /// + /// The type of value to retrieve from the children. + /// The casted values from the children. + public T[] SelectArray(string label) + { + return GetChild(label).SelectArray(); } /// @@ -470,7 +686,17 @@ public IEnumerable SelectValues() /// The casted values from the children. public IEnumerable SelectValues(int index) { - return this[index].SelectValues(); + return GetChild(index).SelectValues(); + } + + /// + /// Selects the casted children values of child AST node marked with specified label. + /// + /// The type of value to retrieve from the children. + /// The casted values from the children. + public IEnumerable SelectValues(string label) + { + return GetChild(label).SelectValues(); } /// @@ -498,7 +724,17 @@ public T[] SelectArray(Func selector) /// The selected values from the children. public T[] SelectArray(int index, Func selector) { - return this[index].SelectArray(selector); + return GetChild(index).SelectArray(selector); + } + + /// + /// Selects the children of child AST node marked with specified label using a selector function. + /// + /// The type of value to retrieve from the children. + /// The selected values from the children. + public T[] SelectArray(string label, Func selector) + { + return GetChild(label).SelectArray(selector); } /// diff --git a/src/RCParsing/ParserRules/SequenceParserRule.cs b/src/RCParsing/ParserRules/SequenceParserRule.cs index ee31438..7cc1672 100644 --- a/src/RCParsing/ParserRules/SequenceParserRule.cs +++ b/src/RCParsing/ParserRules/SequenceParserRule.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using RCParsing.Utils; @@ -11,22 +12,32 @@ namespace RCParsing.ParserRules public class SequenceParserRule : ParserRule { private readonly int[] _rules; - + private readonly Dictionary _labels; + /// /// The rules ids that make up the sequence. /// public IReadOnlyList Rules { get; } + /// + /// The labels for each rule in the sequence. These are used for easier navigation in transformation functions. + /// The key is label, the value is the index of the rule in the sequence. + /// + public IReadOnlyDictionary RuleLabels { get; } + /// /// Initializes a new instance of the class. /// /// The rules ids that make up the sequence. - public SequenceParserRule(IEnumerable parserRules) + /// The labels for each rule in the sequence. + public SequenceParserRule(IEnumerable parserRules, IDictionary labels) { _rules = parserRules?.ToArray() ?? throw new ArgumentNullException(nameof(parserRules)); - Rules = _rules.AsReadOnlyList(); + _labels = labels?.ToDictionary(k => k.Key, v => v.Value) ?? throw new ArgumentNullException(nameof(labels)); if (_rules.Length == 0) throw new ArgumentException("Sequence must have at least one rule"); + Rules = _rules.AsReadOnlyList(); + RuleLabels = new ReadOnlyDictionary(_labels); } protected override HashSet FirstCharsCore diff --git a/src/RCParsing/RCParsing.csproj b/src/RCParsing/RCParsing.csproj index 5ba2fe8..659256b 100644 --- a/src/RCParsing/RCParsing.csproj +++ b/src/RCParsing/RCParsing.csproj @@ -8,7 +8,7 @@ RCParsing - 5.2.1 + 5.3.0 Roman K. RomeCore RCParsing diff --git a/src/RCParsing/SemanticException.cs b/src/RCParsing/SemanticException.cs index 6883f82..248f77e 100644 --- a/src/RCParsing/SemanticException.cs +++ b/src/RCParsing/SemanticException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -103,10 +103,10 @@ private static string FormatMessage(ParsedRuleResultBase result, string message, out int lineStart2, out int lineLength2, out int lineNumber2, out int columnNumber2, out int visualColumnNumber2, result.Context.parser.MainSettings.tabSize); - sb.AppendLine("Location:"); - if (lineNumber1 == lineNumber2) { + sb.AppendLine("Location:"); + // At the same line string lineAndColumn = $"line {lineNumber1}, column {columnNumber1}, length {result.Length}"; string pointerLine; @@ -120,17 +120,23 @@ private static string FormatMessage(ParsedRuleResultBase result, string message, } else { + sb.AppendLine($"Location (from line {lineNumber1}, column {columnNumber1} to line {lineNumber2}, column {columnNumber2}; length {result.Length} characters):"); + + int maxLineNumberLength = Math.Max(lineNumber1.ToString().Length, lineNumber2.ToString().Length); + // At different lines - sb.Append(lineNumber1.ToString().PadLeft(6) + ": "); + sb.Append(lineNumber1.ToString().PadLeft(maxLineNumberLength) + ": "); sb.AppendLine(input.Substring(lineStart1, lineLength1)); - sb.AppendLine(new string('^', lineLength1 - visualColumnNumber1 - 2).PadLeft(lineLength1)); + sb.Append(new string(' ', maxLineNumberLength + 2)); // + 2 from ": " + sb.AppendLine(new string('^', lineLength1 - visualColumnNumber1 + 1).PadLeft(lineLength1)); if (lineNumber1 + 1 != lineNumber2) sb.AppendLine("..."); - sb.Append(lineNumber2.ToString().PadLeft(6) + ": "); + sb.Append(lineNumber2.ToString().PadLeft(maxLineNumberLength) + ": "); sb.AppendLine(input.Substring(lineStart2, lineLength2)); - sb.AppendLine(new string('^', lineLength2 - visualColumnNumber2 - 2).PadRight(lineLength2)); + sb.Append(new string(' ', maxLineNumberLength + 2)); + sb.AppendLine(new string('^', visualColumnNumber2)); } } diff --git a/tests/RCParsing.Tests/Rules/SequenceLabelRuleTests.cs b/tests/RCParsing.Tests/Rules/SequenceLabelRuleTests.cs new file mode 100644 index 0000000..7c4c0bb --- /dev/null +++ b/tests/RCParsing.Tests/Rules/SequenceLabelRuleTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RCParsing.Building; +using RCParsing.ParserRules; + +namespace RCParsing.Tests.Rules +{ + public class SequenceLabelRuleTests + { + [Fact] + public void SimpleLabelsTest() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .LiteralChoice("Hello", "Hola", "Bonjour").Label("greeting") + .Spaces() // No label + .Number().Label("number") + .EOF() + + .Transform(v => + { + return $"Greeting: {v.GetValue("greeting")}, Number: {v.GetValue("number")}"; + }); + + var parser = builder.Build(); + + var value1 = parser.Parse("Hello 123"); + var value2 = parser.Parse("Hola 456"); + var value3 = parser.Parse("Bonjour 789"); + + Assert.Equal("Greeting: Hello, Number: 123", value1); + Assert.Equal("Greeting: Hola, Number: 456", value2); + Assert.Equal("Greeting: Bonjour, Number: 789", value3); + } + + [Fact] + public void NoLabelFound_ThrowsException() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .LiteralChoice("Hello", "Hola", "Bonjour").Label("greeting") + .Spaces() // No label + .Number().Label("number") + .EOF() + + .Transform(v => + { + return v.GetValue("unknown"); + }); + + var parser = builder.Build(); + + Assert.Throws(() => parser.Parse("Hello 123")); + } + + [Fact] + public void EmptySequence_ThrowsException() + { + Assert.Throws(() => + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Label("empty"); + }); + } + + [Fact] + public void MultipleSameLabels_ThrowsException() + { + Assert.Throws(() => + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Number().Label("float") + .Spaces() + .Number().Label("float"); + }); + } + + [Fact] + public void SingleRule_CreatesSequence() + { + var builder = new ParserBuilder(); + + builder.CreateRule("rule") + .Literal("Hello").Label("greeting"); + + var parser = builder.Build(); + + var rule = parser.GetRule("rule"); + Assert.IsType(rule); + Assert.Equal(2, parser.Rules.Count); // sequence "rule" and token rule that wraps literal token "Hello" + } + } +} diff --git a/tests/RCParsing.Tests/SemanticExceptionTests.cs b/tests/RCParsing.Tests/SemanticExceptionTests.cs new file mode 100644 index 0000000..d568d89 --- /dev/null +++ b/tests/RCParsing.Tests/SemanticExceptionTests.cs @@ -0,0 +1,142 @@ +using Xunit.Abstractions; + +namespace RCParsing.Tests +{ + public class SemanticExceptionTests(ITestOutputHelper output) + { + [Fact] + public void SemanticExceptions_SimpleTest() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Literal("Hello!") + + .Transform(v => + { + throw new SemanticException(v, "Invalid message!"); + }); + + var parser = builder.Build(); + + Assert.True(parser.TryParse("Hello!", out var result)); + var ex = Assert.Throws(() => result.Value); + output.WriteLine(ex.ToString()); + } + + [Fact] + public void SemanticExceptions_SingleCharFormatting() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Literal("a") + + .Transform(v => + { + throw new SemanticException(v, "Invalid message!"); + }); + + var parser = builder.Build(); + + Assert.True(parser.TryParse("a", out var result)); + var ex = Assert.Throws(() => result.Value); + var exstr = ex.ToString(); + output.WriteLine(exstr); + + Assert.Contains("Invalid message!", exstr); + Assert.Contains("line 1", exstr); + Assert.Contains("column 1", exstr); + Assert.DoesNotContain("length", exstr); + } + + [Fact] + public void SemanticExceptions_MultipleCharsFormatting() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Literal("abc") + + .Transform(v => + { + throw new SemanticException(v, "Invalid message!"); + }); + + var parser = builder.Build(); + + Assert.True(parser.TryParse("abc", out var result)); + var ex = Assert.Throws(() => result.Value); + var exstr = ex.ToString(); + output.WriteLine(exstr); + + Assert.Contains("Invalid message!", exstr); + Assert.Contains("line 1", exstr); + Assert.Contains("column 1", exstr); + Assert.Contains("length 3", exstr); + } + + [Fact] + public void SemanticExceptions_DifferentLinesFormatting() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Literal("abc") + .Newline() + .Literal("def") + + .Transform(v => + { + throw new SemanticException(v, "Invalid message!"); + }); + + var parser = builder.Build(); + + Assert.True(parser.TryParse("abc\ndef", out var result)); + var ex = Assert.Throws(() => result.Value); + var exstr = ex.ToString(); + output.WriteLine(exstr); + + Assert.Contains("Invalid message!", exstr); + Assert.Contains("line 1", exstr); + Assert.Contains("column 1", exstr); + Assert.Contains("line 2", exstr); + Assert.Contains("column 3", exstr); + Assert.Contains("length 7", exstr); + } + + [Fact] + public void SemanticExceptions_DifferentLinesFormatting_WithLineGap() + { + var builder = new ParserBuilder(); + + builder.CreateMainRule() + .Literal("abc") + .Newline() + .Literal("def") + .Newline() + .Literal("ghi") + + .Transform(v => + { + throw new SemanticException(v, "Invalid message!"); + }); + + var parser = builder.Build(); + + Assert.True(parser.TryParse("abc\ndef\nghi", out var result)); + var ex = Assert.Throws(() => result.Value); + var exstr = ex.ToString(); + output.WriteLine(exstr); + + Assert.Contains("Invalid message!", exstr); + Assert.Contains("line 1", exstr); + Assert.Contains("column 1", exstr); + Assert.Contains("...", exstr); + Assert.Contains("line 3", exstr); + Assert.Contains("column 3", exstr); + Assert.Contains("length 11", exstr); + } + } +}