diff --git a/README.md b/README.md index a593db9..d0c7dd8 100644 --- a/README.md +++ b/README.md @@ -22,40 +22,40 @@ private static Parser CreateParser() { // Grammar: // ---------------------------------------- - // Start : Sum $ - // Sum : Product (S [+-] Product)* - // Product : Unary (S [*/] Unary)* - // Unary : S '-'? Primary + // Start : S Sum $ + // Sum : Product ([+-] S Product)* + // Product : Unary ([*/] S Unary)* + // Unary : '-'? S Primary // Primary : Parenthesis / Value - // Parenthesis : S '(' Sum S ')' - // Value : S Number - // S : Whitespace* + // Parenthesis : '(' S Sum ')' S + // Value : Number S + // S : [ \r\n\t]* var sum = Deferred(); - var value = S.Then(Literal.Number()); + var value = Literal.Number().ThenIgnore(S); var parenthesis = sum.Between( - Seq(S, L('(')), - Seq(S, L(')')) + Seq(L('('), S), + Seq(L(')'), S) ); var primary = parenthesis.Or(value); var unary = Seq( - S, L('-').Optional(), + S, primary - ).Do((_, u, d) => u.HasValue ? -d : d); + ).Do((u, _, d) => u.HasValue ? -d : d); var product = unary.Fold( - S.Then(OneOf("*/")), + OneOf("*/").ThenIgnore(S), (l, r, op) => op == '*' ? l * r : l / r); sum.Parser = product.Fold( - S.Then(OneOf("+-")), + OneOf("+-").ThenIgnore(S), (l, r, op) => op == '+' ? l + r : l - r); - return sum.Before(Eof); + return sum.Between(S, Eof); } ``` diff --git a/benchmarks/Parsers/RamstackParsers.cs b/benchmarks/Parsers/RamstackParsers.cs index 04668d2..d7946d9 100644 --- a/benchmarks/Parsers/RamstackParsers.cs +++ b/benchmarks/Parsers/RamstackParsers.cs @@ -7,8 +7,8 @@ public static class RamstackParsers { public static readonly Parser EmailParser = CreateEmailParser(); public static readonly Parser EmailVoidParser = EmailParser.Void(); - public static readonly Parser ExpressionParser = Samples.ExpressionParser.Parser; - public static readonly Parser JsonParser = Samples.JsonParser.Parser; + public static readonly Parser ExpressionParser = Samples.CalcExpr.ExpressionParser.Parser; + public static readonly Parser JsonParser = Samples.Json.JsonParser.Parser; private static Parser CreateEmailParser() { diff --git a/samples/CalcExpr/ExpressionParser.cs b/samples/CalcExpr/ExpressionParser.cs new file mode 100644 index 0000000..0adc04c --- /dev/null +++ b/samples/CalcExpr/ExpressionParser.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; + +using Ramstack.Parsing; + +using static Ramstack.Parsing.Parser; + +namespace Samples.CalcExpr; + +public static class ExpressionParser +{ + public static readonly Parser Parser = CreateExpressionParser(); + + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static Parser CreateExpressionParser() + { + var sum_expr = + Deferred(); + + var number_expr = + Literal.Number("number").ThenIgnore(S); + + var parenthesis_expr = + sum_expr.Between( + Seq(L('('), S), + Seq(L(')'), S)); + + var primary_expr = + parenthesis_expr.Or(number_expr); + + var unary_expr = + Seq( + L('-').Optional(), + S, + primary_expr + ).Do((u, _, d) => u.HasValue ? -d : d); + + var mul_expr = + unary_expr.Fold( + OneOf("*/").ThenIgnore(S), + (l, r, o) => o == '*' ? l * r : l / r); + + sum_expr.Parser = + mul_expr.Fold( + OneOf("+-").ThenIgnore(S), + (l, r, o) => o == '+' ? l + r : l - r); + + return sum_expr.Between(S, Eof); + } +} diff --git a/samples/CalcExpr/README.md b/samples/CalcExpr/README.md new file mode 100644 index 0000000..cebaa03 --- /dev/null +++ b/samples/CalcExpr/README.md @@ -0,0 +1,43 @@ +# Simple Calc + +This project implements a simple mathematical expression parser. + +## Simple Expression Grammar + +```sh +start + = sum_expr EOF + ; + +sum_expr + = mul_expr (S [+-] mul_expr)* + ; + +mul_expr + = unary_expr (S [*/] unary_expr)* + ; + +unary_expr + = S "-"? primary_expr + ; + +primary_expr + = parenthesis_expr / number_expr + ; + +parenthesis_expr + = S "(" Sum S ")" + ; + +number_expr + = S [0-9]+ + ; + +S + = [ \t\r\n]* + ; + +EOF: + = $ + ; +``` diff --git a/samples/ExpressionParser.cs b/samples/ExpressionParser.cs deleted file mode 100644 index 66a643e..0000000 --- a/samples/ExpressionParser.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Ramstack.Parsing; - -using static Ramstack.Parsing.Parser; - -namespace Samples; - -public static class ExpressionParser -{ - public static readonly Parser Parser = CreateExpressionParser(); - - private static Parser CreateExpressionParser() - { - // Grammar: - // ---------------------------------------- - // Start : Sum $ - // Sum : Product (S [+-] Product)* - // Product : Unary (S [*/] Unary)* - // Unary : S '-'? Primary - // Primary : Parenthesis / Value - // Parenthesis : S '(' Sum S ')' - // Value : S Number - // S : Whitespace* - - var sum = Deferred(); - var number = S.Then(Literal.Number("number")); - - var parenthesis = sum.Between( - Seq(S, L('(')), - Seq(S, L(')')) - ); - - var primary = parenthesis.Or(number); - - var unary = Seq( - S, - L('-').Optional(), - primary - ).Do((_, u, d) => u.HasValue ? -d : d); - - var product = unary.Fold( - S.Then(OneOf("*/")), - (l, r, o) => o == '*' ? l * r : l / r); - - sum.Parser = product.Fold( - S.Then(OneOf("+-")), - (l, r, o) => o == '+' ? l + r : l - r); - - return sum.Before(Eof); - } -} diff --git a/samples/Json/JsonParser.cs b/samples/Json/JsonParser.cs new file mode 100644 index 0000000..2ade580 --- /dev/null +++ b/samples/Json/JsonParser.cs @@ -0,0 +1,60 @@ +using Ramstack.Parsing; + +using static Ramstack.Parsing.Literal; +using static Ramstack.Parsing.Parser; + +namespace Samples.Json; + +public static class JsonParser +{ + public static readonly Parser Parser = CreateJsonParser(); + + private static Parser CreateJsonParser() + { + var value = + Deferred(); + + var @string = + DoubleQuotedString.Do(object? (s) => s); + + var number = + Number().Do(object? (n) => n); + + var primitive = OneOf(["true", "false", "null"]).Do(s => + { + object? r = null; + if (s.Length != 0 && s[0] != 'n') + r = s[0] == 't'; + return r; + }); + + var array = value + .Separated(Seq(L(','), S)) + .Between( + Seq(L('['), S), + Seq(L(']'), S)) + .Do(object? (list) => list); + + var member = Seq( + DoubleQuotedString, S, L(':'), S, value + ).Do((name, _, _, _, v) => KeyValuePair.Create(name, v)); + + var @object = member + .Separated(Seq(L(','), S)) + .Between( + Seq(L('{'), S), + Seq(L('}'), S)) + .Do(object? (members) => new Dictionary(members)); + + value.Parser = + Choice( + @string, + number, + primitive, + array, + @object + ).ThenIgnore(S); + + return S.Then(value); + } +} diff --git a/samples/Json/README.md b/samples/Json/README.md new file mode 100644 index 0000000..f6cf9ee --- /dev/null +++ b/samples/Json/README.md @@ -0,0 +1,31 @@ +# JSON Parser + +This project implements a simple JSON parser. + +## JSON Grammar + +```sh +start + = value $ + ; + +value + = (object / array / string / number / "true" / "false" / "null") S + ; + +object + = "{" S (member ("," S member)*)? "}" S + ; + +member + = string ":" S value + ; + +array + = "[" S (value ("," S value)*)? "]" S + ; + +S + = [ \t\r\n]* + ; +``` diff --git a/samples/JsonParser.cs b/samples/JsonParser.cs deleted file mode 100644 index aa2526a..0000000 --- a/samples/JsonParser.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Ramstack.Parsing; - -using static Ramstack.Parsing.Literal; -using static Ramstack.Parsing.Parser; - -namespace Samples; - -public static class JsonParser -{ - public static readonly Parser Parser = CreateJsonParser(); - - private static Parser CreateJsonParser() - { - var value = Deferred(); - - var text = DoubleQuotedString.Do(object? (s) => s); - var number = Number().Do(object? (n) => n); - - var primitive = OneOf(["true", "false", "null"]).Do(s => - { - object? r = null; - if (s.Length != 0 && s[0] != 'n') - r = s[0] == 't'; - - return r; - }); - - var array = value - .Separated(Seq(S, L(','))) - .Between( - L('['), - Seq(S, L(']'))) - .Do(object? (list) => list); - - var member = Seq( - S, DoubleQuotedString, S, L(':'), value - ).Do((_, name, _, _, v) => KeyValuePair.Create(name, v)); - - var map = member - .Separated(Seq(S, L(','))) - .Between( - L('{'), - Seq(S, L('}'))) - .Do(object? (members) => new Dictionary(members)); - - value.Parser = S.Then(Choice(text, number, primitive, array, map)); - return value; - } -} diff --git a/samples/TinyC/Node.cs b/samples/TinyC/Node.cs new file mode 100644 index 0000000..709c864 --- /dev/null +++ b/samples/TinyC/Node.cs @@ -0,0 +1,126 @@ +using Samples.Utilities; + +namespace Samples.TinyC; + +public abstract record Node +{ + public static Node Empty() => new BlockNode([]); + public static Node Number(int value) => new NumberNode(value); + public static Node Variable(string name) => new VariableNode(name); + public static Node If(Node test, Node ifTrue, Node ifFalse) => new IfNode(test, Wrap(ifTrue), Wrap(ifFalse)); + public static Node Ternary(Node test, Node ifTrue, Node ifFalse) => new TernaryNode(test, ifTrue, ifFalse); + public static Node WhileLoop(Node test, Node body) => new WhileLoopNode(test, Wrap(body)); + public static Node DoWhileLoop(Node test, Node body) => new DoWhileLoopNode(test, Wrap(body)); + public static Node Unary(char op, Node operand) => new UnaryNode(op, operand); + public static Node Binary(string op, Node left, Node right) => new BinaryNode(op, left, right); + public static Node Assign(Node variable, Node value) => Binary("=", variable, value); + public static Node Block(IReadOnlyList statements) => new BlockNode(statements); + private static Node Wrap(Node statement) => statement is BlockNode ? statement : new BlockNode([statement]); + + public sealed override string ToString() + { + var sb = new IndentedStringBuilder(); + Print(this, sb); + return sb.ToString(); + + static void Print(Node node, IndentedStringBuilder sb) + { + switch (node) + { + case VariableNode(Name: var name): + sb.Append(name); + break; + + case NumberNode(Value: var number): + sb.Append(number.ToString()); + break; + + case IfNode c: + sb.Append("if ("); + Print(c.Test, sb); + sb.Append(')'); + sb.AppendLine(); + + Print(c.IfTrue, sb); + + if (c.IfFalse is BlockNode { Statements: [IfNode @else] }) + { + sb.Append("else "); + Print(@else, sb); + } + else if (c.IfFalse is not BlockNode([])) + { + sb.Append("else"); + sb.AppendLine(); + Print(c.IfFalse, sb); + } + break; + + case TernaryNode c: + Print(c.Test, sb); + sb.Append(" ? ("); + Print(c.IfTrue, sb); + sb.Append(") : ("); + Print(c.IfFalse, sb); + sb.Append(')'); + break; + + case WhileLoopNode w: + sb.Append("while ("); + Print(w.Test, sb); + sb.Append(')'); + sb.AppendLine(); + Print(w.Body, sb); + break; + + case DoWhileLoopNode d: + sb.AppendLine("do"); + Print(d.Body, sb); + sb.Append("while ("); + Print(d.Test, sb); + sb.AppendLine(");"); + break; + + case UnaryNode u: + sb.Append(u.Operator); + sb.Append('('); + Print(u.Operand, sb); + sb.Append(')'); + break; + + case BinaryNode b: + sb.Append('('); + Print(b.Left, sb); + sb.Append($" {b.Operator} "); + Print(b.Right, sb); + sb.Append(')'); + break; + + case BlockNode block: + sb.AppendLine("{"); + sb.IncrementIndent(); + + foreach (var stmt in block.Statements) + { + Print(stmt, sb); + if (stmt is NumberNode or VariableNode or UnaryNode or BinaryNode) + sb.AppendLine(";"); + } + + sb.DecrementIndent(); + sb.AppendLine("}"); + break; + } + } + } + + public sealed record VariableNode(string Name) : Node; + public sealed record NumberNode(int Value) : Node; + public sealed record IfNode(Node Test, Node IfTrue, Node IfFalse) : Node; + public sealed record TernaryNode(Node Test, Node IfTrue, Node IfFalse) : Node; + public sealed record WhileLoopNode(Node Test, Node Body) : Node; + public sealed record DoWhileLoopNode(Node Test, Node Body) : Node; + public sealed record UnaryNode(char Operator, Node Operand) : Node; + public sealed record BinaryNode(string Operator, Node Left, Node Right) : Node; + public sealed record BlockNode(IReadOnlyList Statements) : Node; +} diff --git a/samples/TinyC/README.md b/samples/TinyC/README.md new file mode 100644 index 0000000..1493dc1 --- /dev/null +++ b/samples/TinyC/README.md @@ -0,0 +1,143 @@ +# Tiny-C + +This project implements a parser for the [Tiny-C](http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c) language, a highly simplified version of `C` designed as an educational tool for learning about compilers. + +All variables are predefined, of integer type, and initialized to zero. + +The main differences from the original `Tiny-C` are: +- Variable names are not limited to single letters. +- Additional operators are supported. + +## Tiny-C Grammar + +```sh +start: + = S statement EOF + ; + +keyword + = ("while" / "do" / "if" / "else") ![\w] + ; + +number + = [0-9]+; + +variable + = !keyword [a-zA-Z_][a-zA-Z0-9_]*; + +S + = [ \t\n\r]* + ; + +EOF + = $ + ; + +var_expr + = variable S + ; + +number_expr + = number S + ; + +expr + = assigment_expr + ; + +assigment_expr + = var_expr "=" S expr + / ternary_expr + ; + +ternary_expr + = or_expr ("?" S expr ":" S ternary_expr)? + ; + +or_expr + = and_expr ("||" S and_expr)* + ; + +and_expr + = inclusive_or_expr ("&&" S inclusive_or_expr)* + ; + +inclusive_or_expr + = exclusive_or_expr ("|" S exclusive_or_expr)* + ; + +exclusive_or_expr + = binary_and_expr ("^" S binary_and_expr)* + ; + +binary_and_expr + = eq_expr ("&" S eq_expr)* + ; + +eq_expr + = relational_expr (("==" / "!=") S relational_expr)* + ; + +relational_expr + = shift_expr (("<" / "<=" / ">" / ">=") S shift_expr)* + ; + +shift_expr + = sum_expr (("<<" / ">>") S sum_expr)* + ; + +sum_expr + = mul_expr ([+-] S mul_expr)* + ; + +mul_expr + = unary_expr ([*/%] S unary_expr)* + ; + +unary_expr + = [-+~!]? S primary_expr + ; + +primary_expr + = parenthesis + / var_expr + / number_expr + ; + +parenthesis + = "(" S expr ")" S + ; + +statement + = if_statement + / while_statement + / do_while_statement + / block_statement + / expr_statement + / empty_statement + ; + +if_statement + = "if" S "(" S expr ")" S statement ("else" S statement)? + ; + +while_statement + = "while" S "(" S expr ")" S statement + ; + +do_while_statement + = "do" S statement "while" S "(" S expr ")" S ";" S + ; + +block_statement + = "{" S statement* "}" S + ; + +expr_statement + = expr ";" S + ; + +empty_statement + = ";" S + ; +``` diff --git a/samples/TinyC/TinyCParser.cs b/samples/TinyC/TinyCParser.cs new file mode 100644 index 0000000..a8990e1 --- /dev/null +++ b/samples/TinyC/TinyCParser.cs @@ -0,0 +1,221 @@ +using System.Diagnostics.CodeAnalysis; + +using Ramstack.Parsing; + +using static Ramstack.Parsing.Parser; + +namespace Samples.TinyC; + +public static class TinyCParser +{ + public static readonly Parser Parser = CreateParser(); + + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static Parser CreateParser() + { + var keyword = Seq( + OneOf("while", "do", "if", "else"), + Not(Set("\\w"))); + + var number = Set("0-9") + .OneOrMore() + .Map(Node (m) => Node.Number(int.Parse(m))) + .As("number"); + + var variable = Seq( + Not(keyword), + Set("a-z"), + Set("a-zA-Z_0-9").ZeroOrMore() + ).Map(Identifier).As("variable"); + + var semicolon = + Seq(L(';'), S).Void(); + + var eq = + Seq(L('='), S).Void(); + + var if_keyword = + Seq(L("if"), S).Void(); + + var else_keyword = + Seq(L("else"), S).Void(); + + var while_keyword = + Seq(L("while"), S).Void(); + + var do_keyword = + Seq(L("do"), S).Void(); + + var expr = Deferred(); + + var number_expr = + number.ThenIgnore(S); + + var var_expr = + variable.ThenIgnore(S); + + var parenthesis = + expr.Between( + Seq(L('('), S), + Seq(L(')'), S)); + + var primary_expr = + Choice( + parenthesis, + number_expr, + var_expr); + + var unary_expr = Seq( + OneOf("-+~!").Optional(), + S, + primary_expr + ).Do(CreateUnary); + + var mul_expr = unary_expr.Fold( + OneOf("*/%").ThenIgnore(S), + CreateBinary); + + var sum_expr = mul_expr.Fold( + OneOf("+-").ThenIgnore(S), + CreateBinary); + + var shift_expr = sum_expr.Fold( + OneOf("<<", ">>").ThenIgnore(S), + (l, r, o) => Node.Binary(o, l, r)); + + var relational_expr = shift_expr.Fold( + OneOf("<", "<=", ">", ">=").ThenIgnore(S), + (l, r, o) => Node.Binary(o, l, r)); + + var eq_expr = relational_expr.Fold( + OneOf("==", "!=").ThenIgnore(S), + (l, r, o) => Node.Binary(o, l, r)); + + var binary_and_expr = eq_expr.Fold( + L('&').ThenIgnore(S), + (l, r, _) => Node.Binary("&", l, r)); + + var exclusive_or_expr = binary_and_expr.Fold( + L('^').ThenIgnore(S), + (l, r, _) => Node.Binary("^", l, r)); + + var inclusive_or_expr = exclusive_or_expr.Fold( + L('|').ThenIgnore(S), + (l, r, _) => Node.Binary("|", l, r)); + + var and_expr = inclusive_or_expr.Fold( + L("&&").ThenIgnore(S), + (l, r, _) => Node.Binary("&&", l, r)); + + var or_expr = and_expr.Fold( + L("||").ThenIgnore(S), + (l, r, _) => Node.Binary("||", l, r)); + + var ternary_expr = Deferred(); + ternary_expr.Parser = + Seq( + or_expr, + Seq( + L('?'), S, expr, + L(':'), S, ternary_expr + ).Optional()) + .Do(CreateTernary); + + var assignment_expr = + Choice( + Seq(var_expr, eq, expr).Do(CreateAssign), + ternary_expr); + + expr.Parser = + assignment_expr; + + var statement = + Deferred(); + + var else_clause = + Seq( + else_keyword, + statement + ).Do((_, s) => s).DefaultOnFail(Node.Empty()); + + var if_statement = + Seq( + if_keyword, + parenthesis, + statement, + else_clause + ).Do(CreateIf); + + var block_statement = + statement + .ZeroOrMore() + .Between( + Seq(L('{'), S), + Seq(L('}'), S)) + .Do(CreateBlock); + + var empty_statement = + Seq(L(';'), S + ).Map(_ => Node.Empty()); + + var while_statement = + Seq( + while_keyword, + parenthesis, + statement + ).Do(CreateWhile); + + var do_while_statement = + Seq( + do_keyword, + statement, + while_keyword, + parenthesis, + semicolon + ).Do(CreateDoWhile); + + var expr_statement = + expr.ThenIgnore(semicolon); + + statement.Parser = Choice( + if_statement, + while_statement, + do_while_statement, + block_statement, + expr_statement, + empty_statement + ); + + return statement + .Between(S, Eof); + + static Node Identifier(Match m) => + Node.Variable(m.ToString()); + + static Node CreateUnary(OptionalValue u, Unit _, Node operand) => + u.HasValue ? Node.Unary(u.Value, operand) : operand; + + static Node CreateBinary(Node l, Node r, char o) => + Node.Binary(new string(o, 1), l, r); + + static Node CreateAssign(Node variable, Unit _, Node value) => + Node.Assign(variable, value); + + static Node CreateTernary(Node expression, OptionalValue<(char _1, Unit _2, Node @true, char _4, Unit _5, Node @false)> optional) => + optional.HasValue + ? Node.Ternary(expression, optional.Value.@true, optional.Value.@false) + : expression; + + static Node CreateIf(Unit _, Node expression, Node @true, Node @false) => + Node.If(expression, @true, @false); + + static Node CreateWhile(Unit _, Node expression, Node body) => + Node.WhileLoop(expression, body); + + static Node CreateDoWhile(Unit _0, Node body, Unit _2, Node expression, Unit _4) => + Node.DoWhileLoop(expression, body); + + static Node CreateBlock(IReadOnlyList statements) => + Node.Block(statements); + } +} diff --git a/samples/TinyC/examples/example1.c b/samples/TinyC/examples/example1.c new file mode 100644 index 0000000..92d4f10 --- /dev/null +++ b/samples/TinyC/examples/example1.c @@ -0,0 +1,14 @@ +{ + x = 5; + fact = 1; + + if (x > 0) + { + do + { + fact = fact * x; + x = x - 1; + } + while (x != 0); + } +} diff --git a/samples/TinyC/examples/example2.c b/samples/TinyC/examples/example2.c new file mode 100644 index 0000000..2a097b0 --- /dev/null +++ b/samples/TinyC/examples/example2.c @@ -0,0 +1,11 @@ +{ + i = 100; + while (i) + { + i = i - 1; + } + + i = 0; + while (i * 2 < 100) + i = i + 1; +} diff --git a/samples/TinyC/examples/example3.c b/samples/TinyC/examples/example3.c new file mode 100644 index 0000000..3b76edf --- /dev/null +++ b/samples/TinyC/examples/example3.c @@ -0,0 +1,26 @@ +if (i > 100) +{ + if (j == 0) + a = 1; + else + a = 2; +} +else if (i < 100) +{ + if (j == 0) a = 10; + else if (j == 1) a = 20; + else if (j == 2) a = 30; + else if (j == 3) a = 40; + else if (j == 4) a = 50; + else if (j == 5) a = 60; + else a = 70; +} +else +{ + b = j == 0 ? 10 : + j == 1 ? 20 : + j == 2 ? 30 : + j == 3 ? 40 : + j == 4 ? 50 : + j == 5 ? 60 : 70; +} diff --git a/samples/TinyC/examples/example4.c b/samples/TinyC/examples/example4.c new file mode 100644 index 0000000..d614b20 --- /dev/null +++ b/samples/TinyC/examples/example4.c @@ -0,0 +1,31 @@ +{ + index = a = b = c = d = 0; + + { + d = 199; + + ; + ; + ; + } + + j = 128; + i = 0; + + if (j * 2 == 128 << 1) + { + do + { + i = i + 1; + j = j - 1; + } + while (i <= j); + } + + x = !0; + y = !1; + z = ~x + ~y ? a : b; + n = z + ? a < 0 ? b + 100 * c : c % 100 + 1 + : b > 0 ? x : (y = 23 & -7); +} diff --git a/samples/Utilities/IndentedStringBuilder.cs b/samples/Utilities/IndentedStringBuilder.cs new file mode 100644 index 0000000..37b9d70 --- /dev/null +++ b/samples/Utilities/IndentedStringBuilder.cs @@ -0,0 +1,109 @@ +using System.Text; + +namespace Samples.Utilities; + +internal sealed class IndentedStringBuilder +{ + private int _indent; + private bool _indentPending = true; + + private readonly StringBuilder _sb = new StringBuilder(); + + public int Length => _sb.Length; + + public IndentedStringBuilder Append(string value) + { + DoIndent(); + _sb.Append(value); + return this; + } + + public IndentedStringBuilder Append(FormattableString value) + { + DoIndent(); + _sb.Append(value); + return this; + } + + public IndentedStringBuilder Append(char value) + { + DoIndent(); + _sb.Append(value); + return this; + } + + public IndentedStringBuilder AppendLine() + { + AppendLine(string.Empty); + return this; + } + + public IndentedStringBuilder AppendLine(string value) + { + if (value.Length != 0) + DoIndent(); + + _sb.AppendLine(value); + _indentPending = true; + return this; + } + + public IndentedStringBuilder AppendLine(FormattableString value) + { + DoIndent(); + _sb.Append(value); + _indentPending = true; + return this; + } + + public IndentedStringBuilder Clear() + { + _sb.Clear(); + _indentPending = true; + _indent = 0; + + return this; + } + + public IndentedStringBuilder IncrementIndent() + { + _indent++; + return this; + } + + public IndentedStringBuilder DecrementIndent() + { + if (_indent > 0) + _indent--; + + return this; + } + + public IDisposable Indent() => + new Indenter(this); + + public override string ToString() => + _sb.ToString(); + + private void DoIndent() + { + if (_indentPending && _indent > 0) + _sb.Append(' ', _indent * 4); + + _indentPending = false; + } + + private sealed class Indenter : IDisposable + { + private readonly IndentedStringBuilder _sb; + + public Indenter(IndentedStringBuilder sb) + { + _sb = sb; + _sb.IncrementIndent(); + } + + public void Dispose() => + _sb.DecrementIndent(); + } +} diff --git a/src/Ramstack.Parsing/DeferredParser.cs b/src/Ramstack.Parsing/DeferredParser.cs index 92e0c56..8cf26a5 100644 --- a/src/Ramstack.Parsing/DeferredParser.cs +++ b/src/Ramstack.Parsing/DeferredParser.cs @@ -21,8 +21,8 @@ internal DeferredParser() => /// using the specified function to define the parser. /// /// A function that accepts a reference to this deferred parser and returns the resulting parser. - internal DeferredParser(Func, Parser> parser) => - Parser = parser(this); + internal DeferredParser(Func, Parser> parser) => + Parser = parser(this) ?? throw new InvalidOperationException("The deferred parser has not been initialized."); /// public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out T? value) => diff --git a/src/Ramstack.Parsing/Parser.Deferred.cs b/src/Ramstack.Parsing/Parser.Deferred.cs index 1c9f650..c58c831 100644 --- a/src/Ramstack.Parsing/Parser.Deferred.cs +++ b/src/Ramstack.Parsing/Parser.Deferred.cs @@ -20,6 +20,6 @@ public static DeferredParser Deferred() => /// /// A parser that can be recursively defined. /// - public static Parser Recursive(Func, Parser> parser) => + public static Parser Recursive(Func, Parser> parser) => new DeferredParser(parser); } diff --git a/src/Ramstack.Parsing/Parser.Before.cs b/src/Ramstack.Parsing/Parser.ThenIgnore.cs similarity index 84% rename from src/Ramstack.Parsing/Parser.Before.cs rename to src/Ramstack.Parsing/Parser.ThenIgnore.cs index 305b35a..45334c3 100644 --- a/src/Ramstack.Parsing/Parser.Before.cs +++ b/src/Ramstack.Parsing/Parser.ThenIgnore.cs @@ -14,8 +14,8 @@ partial class Parser /// A parser that sequentially applies the current parser and a specified second parser, /// returning the result of the first parser with ignoring the result of the second. /// - public static Parser Before(this Parser parser, Parser before) => - new BeforeParser(parser, before.Void()); + public static Parser ThenIgnore(this Parser parser, Parser before) => + new ThenIgnoreParser(parser, before.Void()); #region Inner type: BeforeParser @@ -26,7 +26,7 @@ public static Parser Before(this Parser parser, Pa /// The type of the value produced by the first parser. /// The initial parser whose result is returned. /// The subsequent parser, applied after the initial parser. - private sealed class BeforeParser(Parser parser, Parser before) : Parser + private sealed class ThenIgnoreParser(Parser parser, Parser before) : Parser { /// public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out T? value) @@ -50,7 +50,7 @@ public override bool TryParse(ref ParseContext context, [NotNullWhen(true)] out /// protected internal override Parser ToVoidParser() => - new BeforeParser(parser.Void(), before); + new ThenIgnoreParser(parser.Void(), before); } #endregion diff --git a/tests/Ramstack.Parsing.Tests/LiteralTests.UnicodeEscapeSequence.cs b/tests/Ramstack.Parsing.Tests/LiteralTests.UnicodeEscapeSequence.cs index f95f619..7f9681f 100644 --- a/tests/Ramstack.Parsing.Tests/LiteralTests.UnicodeEscapeSequence.cs +++ b/tests/Ramstack.Parsing.Tests/LiteralTests.UnicodeEscapeSequence.cs @@ -19,7 +19,7 @@ partial class LiteralTests [TestCase(@"\uDCBA", '\uDCBA')] public void UnicodeEscapeSequenceTest(string input, char symbol) { - var parser = UnicodeEscapeSequence.Before(Eof); + var parser = UnicodeEscapeSequence.ThenIgnore(Eof); Assert.That(parser.Parse(input).Success, Is.True); Assert.That(parser.Map(m => (m.Index, m.Length)).Parse(input).Value, Is.EqualTo((0, 6))); Assert.That(parser.Parse(input).Value, Is.EqualTo(symbol)); @@ -42,7 +42,7 @@ public void UnicodeEscapeSequenceTest(string input, char symbol) [TestCase(@"\u000я")] public void UnicodeEscapeSequence_InvalidInput(string input) { - var parser = UnicodeEscapeSequence.Before(Eof); + var parser = UnicodeEscapeSequence.ThenIgnore(Eof); Assert.That(parser.Parse(input).Success, Is.False); } } diff --git a/tests/Ramstack.Parsing.Tests/ParsersTests.Before.cs b/tests/Ramstack.Parsing.Tests/ParsersTests.ThenIgnore.cs similarity index 83% rename from tests/Ramstack.Parsing.Tests/ParsersTests.Before.cs rename to tests/Ramstack.Parsing.Tests/ParsersTests.ThenIgnore.cs index 2aea69a..e8f1f69 100644 --- a/tests/Ramstack.Parsing.Tests/ParsersTests.Before.cs +++ b/tests/Ramstack.Parsing.Tests/ParsersTests.ThenIgnore.cs @@ -5,9 +5,9 @@ namespace Ramstack.Parsing; partial class ParsersTests { [Test] - public void BeforeTest() + public void ThenIgnoreTest() { - var parser = Any.Before(Any); + var parser = Any.ThenIgnore(Any); Assert.That(parser.Parse("12").Success, Is.True); Assert.That(parser.Map(m => (m.Index, m.Length)).Parse("12").Value, Is.EqualTo((0, 1))); diff --git a/tests/Ramstack.Parsing.Tests/Scenarios/JsonTests.cs b/tests/Ramstack.Parsing.Tests/Scenarios/JsonTests.cs index 88f7816..32c761c 100644 --- a/tests/Ramstack.Parsing.Tests/Scenarios/JsonTests.cs +++ b/tests/Ramstack.Parsing.Tests/Scenarios/JsonTests.cs @@ -1,6 +1,6 @@ using System.Text.Json; -using Samples; +using Samples.Json; namespace Ramstack.Parsing.Scenarios; diff --git a/tests/Ramstack.Parsing.Tests/Scenarios/SimpleCalcTests.cs b/tests/Ramstack.Parsing.Tests/Scenarios/SimpleCalcTests.cs index 06b15ed..6e42a73 100644 --- a/tests/Ramstack.Parsing.Tests/Scenarios/SimpleCalcTests.cs +++ b/tests/Ramstack.Parsing.Tests/Scenarios/SimpleCalcTests.cs @@ -36,49 +36,67 @@ public class SimpleCalcTests [TestCase("()", 0, "(1:2) Expected [0-9] or '('")] public void SimpleCalcTest(string text, double number, string error = "") { - // Expr : Sum S EOF - // Sum : Product (S [+-] Product)* - // Product : Primary (S [*/] Primary)* + // Expr : S Sum EOF + // Sum : Product ([+-] S Product)* + // Product : Primary ([*/] S Primary)* // Primary : Parenthesis / Value - // Parenthesis : S '(' Sum S ')' - // Value : S Number + // Parenthesis : '(' S Sum ')' S + // Value : Number S var sum = Deferred(); - // S ['0'..'9']+ ('.' ['0'..'9']+)? - var digit = Set("0-9"); - var value = S.Then( + var digit = + Set("0-9"); + + var value = Seq( digit.OneOrMore(), - Seq(L('.'), digit.OneOrMore()).Optional()) - ).Map(m => double.Parse(m, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture)); - - // S '(' Sum S ')' - var parenthesis = sum.Between(Seq(S, L('(')), Seq(S, L(')'))); - - // value / parenthesis - var primary = Choice(value, parenthesis); + Seq(L('.'), digit.OneOrMore()).Optional() + ).ThenIgnore(S).Map(m => + double.Parse(m, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture)); - // Primary (S [*/] Primary)* - var product = Seq(primary, Seq(S, OneOf("*/"), primary).Many()).Do(Multiply); + var parenthesis = + sum.Between( + Seq(L('('), S), + Seq(L(')'), S)); - // Product (S [+-] Product)* - sum.Parser = Seq(product, Seq(S, OneOf("+-"), product).Many()).Do(Add); + var primary = + Choice(value, parenthesis); - // Sum S EOF - var expression = sum.Before(Seq(S, Eof)); - - static double Multiply(double v, ArrayList<(Unit, char, double)> results) + var product = + Seq( + primary, + Seq( + OneOf("*/"), + S, + primary + ).Many() + ).Do(Multiply); + + sum.Parser = + Seq( + product, + Seq( + OneOf("+-"), + S, + product + ).Many() + ).Do(Add); + + var expression = + sum.Between(S, Eof); + + static double Multiply(double v, ArrayList<(char, Unit, double)> results) { - foreach (var (_, op, d) in results) + foreach (var (op, _, d) in results) v = op == '*' ? v * d : v / d; return v; } - static double Add(double v, ArrayList<(Unit, char, double)> results) + static double Add(double v, ArrayList<(char, Unit, double)> results) { - foreach (var (_, op, d) in results) + foreach (var (op, _, d) in results) v = op == '+' ? v + d : v - d; return v;