From 748e7988ad323175f1283502c6390d8af5e33a0f Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 7 Apr 2021 19:57:28 +0300 Subject: [PATCH 01/11] create benchmark --- Fluid.Benchmarks/TagBenchmarks.cs | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 Fluid.Benchmarks/TagBenchmarks.cs diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs new file mode 100644 index 00000000..fc8b016c --- /dev/null +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -0,0 +1,92 @@ +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; + +namespace Fluid.Benchmarks +{ + [MemoryDiagnoser] + public class TagBenchmarks + { + private static readonly FluidParser _parser = new(); + private readonly TemplateContext _context; + private readonly TestCase _rawTag; + private readonly TestCase _ifWithAnds; + private readonly TestCase _ifWithOrs; + private readonly TestCase _elseIf; + + public TagBenchmarks() + { + _rawTag = new TestCase("before {% raw %} {{ TEST 3 }} {% endraw %} after"); + _ifWithAnds = new TestCase("{% if true and false and false == false %}HIDDEN{% endif %}"); + _ifWithOrs = new TestCase("{% if true == false or false or false %}HIDDEN{% endif %}"); + _elseIf = new TestCase("{% if false %}{% elsif true == false or false or false %}HIDDEN{% endif %}"); + + _context = new TemplateContext(); + } + + [Benchmark] + public object RawTag_Parse() + { + return _rawTag.Parse(); + } + + [Benchmark] + public string RawTag_Render() + { + return _rawTag.Render(_context); + } + + [Benchmark] + public object IfStatement_Ands_Parse() + { + return _ifWithAnds.Parse(); + } + + [Benchmark] + public string IfStatement_Ands_Render() + { + return _ifWithAnds.Render(_context); + } + + [Benchmark] + public object IfStatement_Ors_Parse() + { + return _ifWithOrs.Parse(); + } + + [Benchmark] + public string IfStatement_Ors_Render() + { + return _ifWithOrs.Render(_context); + } + + [Benchmark] + public object ElseIfStatement_Parse() + { + return _elseIf.Parse(); + } + + [Benchmark] + public string ElseIfStatement_Render() + { + return _elseIf.Render(_context); + } + + private sealed class TestCase + { + private readonly string _source; + private readonly IFluidTemplate _template; + + public TestCase(string source) + { + _source = source; + _template = _parser.Parse(source); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Render(TemplateContext context) => _template.Render(context); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IFluidTemplate Parse() => _parser.Parse(_source); + } + } +} \ No newline at end of file From 99b2e02b2c7faa1de0bd777e76e4531b6d4377c1 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 20:02:08 +0300 Subject: [PATCH 02/11] add assign test --- Fluid.Benchmarks/TagBenchmarks.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs index fc8b016c..befb28ae 100644 --- a/Fluid.Benchmarks/TagBenchmarks.cs +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -12,6 +12,7 @@ public class TagBenchmarks private readonly TestCase _ifWithAnds; private readonly TestCase _ifWithOrs; private readonly TestCase _elseIf; + private readonly TestCase _assign; public TagBenchmarks() { @@ -19,6 +20,7 @@ public TagBenchmarks() _ifWithAnds = new TestCase("{% if true and false and false == false %}HIDDEN{% endif %}"); _ifWithOrs = new TestCase("{% if true == false or false or false %}HIDDEN{% endif %}"); _elseIf = new TestCase("{% if false %}{% elsif true == false or false or false %}HIDDEN{% endif %}"); + _assign = new TestCase("{% assign something = 'foo' %} {% assign another = 1234 %} {% assign last = something %}"); _context = new TemplateContext(); } @@ -71,6 +73,18 @@ public string ElseIfStatement_Render() return _elseIf.Render(_context); } + [Benchmark] + public object Assign_Parse() + { + return _assign.Parse(); + } + + [Benchmark] + public string Assign_Render() + { + return _assign.Render(_context); + } + private sealed class TestCase { private readonly string _source; From 945c26d05bf350e1e3e3e2e8381fd9c73d494fce Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 20:05:50 +0300 Subject: [PATCH 03/11] add else benchmark --- Fluid.Benchmarks/TagBenchmarks.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs index befb28ae..089ffc25 100644 --- a/Fluid.Benchmarks/TagBenchmarks.cs +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -13,6 +13,7 @@ public class TagBenchmarks private readonly TestCase _ifWithOrs; private readonly TestCase _elseIf; private readonly TestCase _assign; + private readonly TestCase _else; public TagBenchmarks() { @@ -20,6 +21,7 @@ public TagBenchmarks() _ifWithAnds = new TestCase("{% if true and false and false == false %}HIDDEN{% endif %}"); _ifWithOrs = new TestCase("{% if true == false or false or false %}HIDDEN{% endif %}"); _elseIf = new TestCase("{% if false %}{% elsif true == false or false or false %}HIDDEN{% endif %}"); + _else = new TestCase("{% if false %}{% else %}SHOWN{% endif %}"); _assign = new TestCase("{% assign something = 'foo' %} {% assign another = 1234 %} {% assign last = something %}"); _context = new TemplateContext(); @@ -85,6 +87,18 @@ public string Assign_Render() return _assign.Render(_context); } + [Benchmark] + public object Else_Parse() + { + return _else.Parse(); + } + + [Benchmark] + public string Else_Render() + { + return _else.Render(_context); + } + private sealed class TestCase { private readonly string _source; From 79508e828e0efa4549dc822e94f6a5fd3dd2bf31 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 20:08:34 +0300 Subject: [PATCH 04/11] add TextSpan benchmark --- Fluid.Benchmarks/TagBenchmarks.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs index 089ffc25..952ec761 100644 --- a/Fluid.Benchmarks/TagBenchmarks.cs +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -14,6 +14,7 @@ public class TagBenchmarks private readonly TestCase _elseIf; private readonly TestCase _assign; private readonly TestCase _else; + private readonly TestCase _textSpan; public TagBenchmarks() { @@ -23,6 +24,7 @@ public TagBenchmarks() _elseIf = new TestCase("{% if false %}{% elsif true == false or false or false %}HIDDEN{% endif %}"); _else = new TestCase("{% if false %}{% else %}SHOWN{% endif %}"); _assign = new TestCase("{% assign something = 'foo' %} {% assign another = 1234 %} {% assign last = something %}"); + _textSpan = new TestCase("foo"); _context = new TemplateContext(); } @@ -99,6 +101,18 @@ public string Else_Render() return _else.Render(_context); } + [Benchmark] + public object TextSpan_Parse() + { + return _textSpan.Parse(); + } + + [Benchmark] + public string TextSpan_Render() + { + return _textSpan.Render(_context); + } + private sealed class TestCase { private readonly string _source; From cd50d3c2b0c8562daf8e78f22fc63244d7c1c377 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 20:08:34 +0300 Subject: [PATCH 05/11] add TextSpan benchmark --- Fluid.Benchmarks/TagBenchmarks.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs index 952ec761..6733685f 100644 --- a/Fluid.Benchmarks/TagBenchmarks.cs +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -4,6 +4,7 @@ namespace Fluid.Benchmarks { [MemoryDiagnoser] + [ShortRunJob] public class TagBenchmarks { private static readonly FluidParser _parser = new(); From 49d56637b24f120dfbf3558462e3250c44abf0de Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 8 Apr 2021 17:31:04 +0300 Subject: [PATCH 06/11] Reduce state machines in RawStatement --- Fluid/Ast/RawStatement.cs | 20 ++++++++++++-------- Fluid/ExceptionHelper.cs | 8 +++++++- Fluid/TemplateContext.cs | 5 +++-- Fluid/TemplateOptions.cs | 2 +- Fluid/Utils/TaskExtensions.cs | 16 ++++++++++++++++ 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 Fluid/Utils/TaskExtensions.cs diff --git a/Fluid/Ast/RawStatement.cs b/Fluid/Ast/RawStatement.cs index 4c7ab329..c7f3a1a8 100644 --- a/Fluid/Ast/RawStatement.cs +++ b/Fluid/Ast/RawStatement.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Fluid.Utils; namespace Fluid.Ast { @@ -16,17 +17,20 @@ public RawStatement(in TextSpan text) public ref readonly TextSpan Text => ref _text; - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { + static async ValueTask Awaited(Task task) + { + await task; + return Completion.Normal; + } + context.IncrementSteps(); -#if NETSTANDARD2_0 - await writer.WriteAsync(_text.ToString()); -#else - await writer.WriteAsync(_text.Span.ToArray()); -#endif - - return Completion.Normal; + var task = writer.WriteAsync(_text.ToString()); + return task.IsCompletedSuccessfully() + ? new ValueTask(Completion.Normal) + : Awaited(task); } } } \ No newline at end of file diff --git a/Fluid/ExceptionHelper.cs b/Fluid/ExceptionHelper.cs index 60ac6875..336d7931 100644 --- a/Fluid/ExceptionHelper.cs +++ b/Fluid/ExceptionHelper.cs @@ -29,9 +29,15 @@ public static void ThrowParseException(string message) throw new ParseException(message); } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowMaximumStatementsException() + { + throw new InvalidOperationException("The maximum number of statements has been reached. Your script took too long to run."); + } + #if NETSTANDARD2_0 private class DoesNotReturnAttribute : Attribute {} #endif - } } \ No newline at end of file diff --git a/Fluid/TemplateContext.cs b/Fluid/TemplateContext.cs index 20e2be5e..3340a825 100644 --- a/Fluid/TemplateContext.cs +++ b/Fluid/TemplateContext.cs @@ -74,9 +74,10 @@ public TemplateContext(object model) : this() internal void IncrementSteps() { - if (Options.MaxSteps != 0 && _steps++ > Options.MaxSteps) + var maxSteps = Options.MaxSteps; + if (maxSteps > 0 && _steps++ > maxSteps) { - throw new InvalidOperationException("The maximum number of statements has been reached. Your script took too long to run."); + ExceptionHelper.ThrowMaximumStatementsException(); } } diff --git a/Fluid/TemplateOptions.cs b/Fluid/TemplateOptions.cs index c401fa52..e83bc827 100644 --- a/Fluid/TemplateOptions.cs +++ b/Fluid/TemplateOptions.cs @@ -23,7 +23,7 @@ public class TemplateOptions /// /// Gets or sets the maximum number of steps a script can execute. Leave to 0 for unlimited. /// - public int MaxSteps { get; set; } = 0; + public int MaxSteps { get; set; } /// /// Gets or sets the instance used to render locale values like dates and numbers. diff --git a/Fluid/Utils/TaskExtensions.cs b/Fluid/Utils/TaskExtensions.cs new file mode 100644 index 00000000..8b470ba0 --- /dev/null +++ b/Fluid/Utils/TaskExtensions.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Fluid.Utils +{ + internal static class TaskExtensions + { + public static bool IsCompletedSuccessfully(this Task t) + { +#if !NETSTANDARD2_0 + return t.IsCompletedSuccessfully; +#else + return t.Status == TaskStatus.RanToCompletion && !t.IsFaulted && !t.IsCanceled; +#endif + } + } +} \ No newline at end of file From f074fd015a5710a540653d417ff5cdf99be978b6 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 13:43:31 +0300 Subject: [PATCH 07/11] Reduce state machines in AndBinaryExpression, OrBinaryExpression, ElseIfStatement --- Fluid/Ast/BinaryExpression.cs | 2 +- .../BinaryExpressions/AndBinaryExpression.cs | 21 ++++++++-- .../BinaryExpressions/OrBinaryExpression.cs | 21 ++++++++-- Fluid/Ast/ElseIfStatement.cs | 39 ++++++++++++++++++- Fluid/Ast/IfStatement.cs | 3 +- Fluid/Ast/IncrementStatement.cs | 4 +- Fluid/Ast/RawStatement.cs | 4 +- 7 files changed, 77 insertions(+), 17 deletions(-) diff --git a/Fluid/Ast/BinaryExpression.cs b/Fluid/Ast/BinaryExpression.cs index ef105585..8a38fb44 100644 --- a/Fluid/Ast/BinaryExpression.cs +++ b/Fluid/Ast/BinaryExpression.cs @@ -2,7 +2,7 @@ { public abstract class BinaryExpression : Expression { - public BinaryExpression(Expression left, Expression right) + protected BinaryExpression(Expression left, Expression right) { Left = left; Right = right; diff --git a/Fluid/Ast/BinaryExpressions/AndBinaryExpression.cs b/Fluid/Ast/BinaryExpressions/AndBinaryExpression.cs index 6498d316..aa961975 100644 --- a/Fluid/Ast/BinaryExpressions/AndBinaryExpression.cs +++ b/Fluid/Ast/BinaryExpressions/AndBinaryExpression.cs @@ -9,12 +9,25 @@ public AndBinaryExpression(Expression left, Expression right) : base(left, right { } - public override async ValueTask EvaluateAsync(TemplateContext context) + public override ValueTask EvaluateAsync(TemplateContext context) { - var leftValue = await Left.EvaluateAsync(context); - var rightValue = await Right.EvaluateAsync(context); + static async ValueTask Awaited(ValueTask leftTask, ValueTask rightTask) + { + var leftValue = await leftTask; + var rightValue = await rightTask; - return BooleanValue.Create(leftValue.ToBooleanValue() && rightValue.ToBooleanValue()); + return BooleanValue.Create(leftValue.ToBooleanValue() && rightValue.ToBooleanValue()); + } + + var leftTask = Left.EvaluateAsync(context); + var rightTask = Right.EvaluateAsync(context); + + if (leftTask.IsCompletedSuccessfully && rightTask.IsCompletedSuccessfully) + { + return BooleanValue.Create(leftTask.Result.ToBooleanValue() && rightTask.Result.ToBooleanValue()); + } + + return Awaited(leftTask, rightTask); } } } diff --git a/Fluid/Ast/BinaryExpressions/OrBinaryExpression.cs b/Fluid/Ast/BinaryExpressions/OrBinaryExpression.cs index d8ba68af..985a5917 100644 --- a/Fluid/Ast/BinaryExpressions/OrBinaryExpression.cs +++ b/Fluid/Ast/BinaryExpressions/OrBinaryExpression.cs @@ -9,12 +9,25 @@ public OrBinaryExpression(Expression left, Expression right) : base(left, right) { } - public override async ValueTask EvaluateAsync(TemplateContext context) + public override ValueTask EvaluateAsync(TemplateContext context) { - var leftValue = await Left.EvaluateAsync(context); - var rightValue = await Right.EvaluateAsync(context); + static async ValueTask Awaited(ValueTask leftTask, ValueTask rightTask) + { + var leftValue = await leftTask; + var rightValue = await rightTask; - return BooleanValue.Create(leftValue.ToBooleanValue() || rightValue.ToBooleanValue()); + return BooleanValue.Create(leftValue.ToBooleanValue() || rightValue.ToBooleanValue()); + } + + var leftTask = Left.EvaluateAsync(context); + var rightTask = Right.EvaluateAsync(context); + + if (leftTask.IsCompletedSuccessfully && rightTask.IsCompletedSuccessfully) + { + return BooleanValue.Create(leftTask.Result.ToBooleanValue() || rightTask.Result.ToBooleanValue()); + } + + return Awaited(leftTask, rightTask); } } } diff --git a/Fluid/Ast/ElseIfStatement.cs b/Fluid/Ast/ElseIfStatement.cs index bb9cd28d..8d6231f3 100644 --- a/Fluid/Ast/ElseIfStatement.cs +++ b/Fluid/Ast/ElseIfStatement.cs @@ -14,15 +14,50 @@ public ElseIfStatement(Expression condition, List statements) : base( public Expression Condition { get; } - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { // Process statements until next block or end of statements for (var index = 0; index < _statements.Count; index++) { context.IncrementSteps(); - var completion = await _statements[index].WriteToAsync(writer, encoder, context); + var task = _statements[index].WriteToAsync(writer, encoder, context); + if (!task.IsCompletedSuccessfully) + { + return Awaited(task, index + 1, writer, encoder, context); + } + + var completion = task.Result; + if (completion != Completion.Normal) + { + // Stop processing the block statements + // We return the completion to flow it to the outer loop + return new ValueTask(completion); + } + } + return new ValueTask(Completion.Normal); + } + + private async ValueTask Awaited( + ValueTask task, + int startIndex, + TextWriter writer, + TextEncoder encoder, + TemplateContext context) + { + var completion = await task; + if (completion != Completion.Normal) + { + // Stop processing the block statements + // We return the completion to flow it to the outer loop + return completion; + } + // Process statements until next block or end of statements + for (var index = startIndex; index < _statements.Count; index++) + { + context.IncrementSteps(); + completion = await _statements[index].WriteToAsync(writer, encoder, context); if (completion != Completion.Normal) { // Stop processing the block statements diff --git a/Fluid/Ast/IfStatement.cs b/Fluid/Ast/IfStatement.cs index 3c334a6b..d3c0eadc 100644 --- a/Fluid/Ast/IfStatement.cs +++ b/Fluid/Ast/IfStatement.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; diff --git a/Fluid/Ast/IncrementStatement.cs b/Fluid/Ast/IncrementStatement.cs index b768f2ea..67f963b0 100644 --- a/Fluid/Ast/IncrementStatement.cs +++ b/Fluid/Ast/IncrementStatement.cs @@ -26,8 +26,8 @@ public override ValueTask WriteToAsync(TextWriter writer, TextEncode var prefixedIdentifier = Prefix + Identifier; var value = context.GetValue(prefixedIdentifier); - - if (value.IsNil()) + + if (value.IsNil()) { value = NumberValue.Zero; } diff --git a/Fluid/Ast/RawStatement.cs b/Fluid/Ast/RawStatement.cs index c7f3a1a8..8d7e5ca6 100644 --- a/Fluid/Ast/RawStatement.cs +++ b/Fluid/Ast/RawStatement.cs @@ -24,11 +24,11 @@ static async ValueTask Awaited(Task task) await task; return Completion.Normal; } - + context.IncrementSteps(); var task = writer.WriteAsync(_text.ToString()); - return task.IsCompletedSuccessfully() + return task.IsCompletedSuccessfully() ? new ValueTask(Completion.Normal) : Awaited(task); } From dd5e8ca67d80563bb64864e845e2869f78e2a7cf Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 14:19:16 +0300 Subject: [PATCH 08/11] Remove state machines from ElseIfStatement and IfStatement --- Fluid/Ast/ElseIfStatement.cs | 6 +- Fluid/Ast/ElseStatement.cs | 45 ++++++++++++- Fluid/Ast/IfStatement.cs | 120 +++++++++++++++++++++++++++++++---- 3 files changed, 152 insertions(+), 19 deletions(-) diff --git a/Fluid/Ast/ElseIfStatement.cs b/Fluid/Ast/ElseIfStatement.cs index 8d6231f3..56227201 100644 --- a/Fluid/Ast/ElseIfStatement.cs +++ b/Fluid/Ast/ElseIfStatement.cs @@ -17,14 +17,14 @@ public ElseIfStatement(Expression condition, List statements) : base( public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { // Process statements until next block or end of statements - for (var index = 0; index < _statements.Count; index++) + for (var i = 0; i < _statements.Count; i++) { context.IncrementSteps(); - var task = _statements[index].WriteToAsync(writer, encoder, context); + var task = _statements[i].WriteToAsync(writer, encoder, context); if (!task.IsCompletedSuccessfully) { - return Awaited(task, index + 1, writer, encoder, context); + return Awaited(task, i + 1, writer, encoder, context); } var completion = task.Result; diff --git a/Fluid/Ast/ElseStatement.cs b/Fluid/Ast/ElseStatement.cs index 5ca9bac4..a407e04e 100644 --- a/Fluid/Ast/ElseStatement.cs +++ b/Fluid/Ast/ElseStatement.cs @@ -11,14 +11,53 @@ public ElseStatement(List statements) : base(statements) { } - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { for (var i = 0; i < _statements.Count; i++) { context.IncrementSteps(); - var statement = _statements[i]; - var completion = await statement.WriteToAsync(writer, encoder, context); + var task = _statements[i].WriteToAsync(writer, encoder, context); + + if (!task.IsCompletedSuccessfully) + { + return Awaited(task, i + 1, writer, encoder, context); + } + + var completion = task.Result; + + if (completion != Completion.Normal) + { + // Stop processing the block statements + // We return the completion to flow it to the outer loop + return new ValueTask(completion); + } + } + + return new ValueTask(Completion.Normal); + } + + private async ValueTask Awaited( + ValueTask task, + int startIndex, + TextWriter writer, + TextEncoder encoder, + TemplateContext context) + { + var completion = await task; + + if (completion != Completion.Normal) + { + // Stop processing the block statements + // We return the completion to flow it to the outer loop + return completion; + } + + for (var i = startIndex; i < _statements.Count; i++) + { + context.IncrementSteps(); + + completion = await _statements[i].WriteToAsync(writer, encoder, context); if (completion != Completion.Normal) { diff --git a/Fluid/Ast/IfStatement.cs b/Fluid/Ast/IfStatement.cs index d3c0eadc..f8bebcd0 100644 --- a/Fluid/Ast/IfStatement.cs +++ b/Fluid/Ast/IfStatement.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Fluid.Values; namespace Fluid.Ast { @@ -26,13 +27,87 @@ public IfStatement( public IReadOnlyList ElseIfs => _elseIfStatements; - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { - var result = (await Condition.EvaluateAsync(context)).ToBooleanValue(); + var conditionTask = Condition.EvaluateAsync(context); + if (conditionTask.IsCompletedSuccessfully) + { + var result = conditionTask.Result.ToBooleanValue(); + + if (result) + { + for (var i = 0; i < _statements.Count; i++) + { + var statement = _statements[i]; + var task = statement.WriteToAsync(writer, encoder, context); + if (!task.IsCompletedSuccessfully) + { + return Awaited(conditionTask, writer, encoder, context, i + 1); + } + + var completion = task.Result; + + if (completion != Completion.Normal) + { + // Stop processing the block statements + // We return the completion to flow it to the outer loop + return new ValueTask(completion); + } + } + + return new ValueTask(Completion.Normal); + } + else + { + for (var i = 0; i < _elseIfStatements.Count; i++) + { + var elseIf = _elseIfStatements[i]; + var elseIfConditionTask = elseIf.Condition.EvaluateAsync(context); + if (!elseIfConditionTask.IsCompletedSuccessfully) + { + var writeTask = elseIf.WriteToAsync(writer, encoder, context); + return AwaitedElseBranch(elseIfConditionTask, writeTask, writer, encoder, context, i + 1); + } + + if (elseIfConditionTask.Result.ToBooleanValue()) + { + var writeTask = elseIf.WriteToAsync(writer, encoder, context); + if (!writeTask.IsCompletedSuccessfully) + { + return AwaitedElseBranch(elseIfConditionTask, writeTask, writer, encoder, context, i + 1); + } + + return new ValueTask(writeTask.Result); + } + } + + if (Else != null) + { + return Else.WriteToAsync(writer, encoder, context); + } + } + + return new ValueTask(Completion.Normal); + } + else + { + return Awaited(conditionTask, writer, encoder, context, statementStartIndex: 0); + } + } + + + private async ValueTask Awaited( + ValueTask conditionTask, + TextWriter writer, + TextEncoder encoder, + TemplateContext context, + int statementStartIndex) + { + var result = (await conditionTask).ToBooleanValue(); if (result) { - for (var i = 0; i < _statements.Count; i++) + for (var i = statementStartIndex; i < _statements.Count; i++) { var statement = _statements[i]; var completion = await statement.WriteToAsync(writer, encoder, context); @@ -49,21 +124,40 @@ public override async ValueTask WriteToAsync(TextWriter writer, Text } else { - for (var i = 0; i < _elseIfStatements.Count; i++) - { - var elseIf = _elseIfStatements[i]; - if ((await elseIf.Condition.EvaluateAsync(context)).ToBooleanValue()) - { - return await elseIf.WriteToAsync(writer, encoder, context); - } - } + await AwaitedElseBranch(new ValueTask(BooleanValue.False), null, writer, encoder, context, startIndex: 0); + } - if (Else != null) + return Completion.Normal; + } + + private async ValueTask AwaitedElseBranch( + ValueTask conditionTask, + ValueTask elseIfTask, + TextWriter writer, + TextEncoder encoder, + TemplateContext context, + int startIndex) + { + bool condition = (await conditionTask).ToBooleanValue(); + if (condition) + { + await elseIfTask; + } + + for (var i = startIndex; i < _elseIfStatements.Count; i++) + { + var elseIf = _elseIfStatements[i]; + if ((await elseIf.Condition.EvaluateAsync(context)).ToBooleanValue()) { - await Else.WriteToAsync(writer, encoder, context); + return await elseIf.WriteToAsync(writer, encoder, context); } } + if (Else != null) + { + return await Else.WriteToAsync(writer, encoder, context); + } + return Completion.Normal; } } From c45123ece6a7c02e6161cef1f15b3a259d11bea6 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 14:43:58 +0300 Subject: [PATCH 09/11] remove state machines from TextSpanStatement --- Fluid/Ast/IfStatement.cs | 2 +- Fluid/Ast/TextSpanStatement.cs | 38 ++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Fluid/Ast/IfStatement.cs b/Fluid/Ast/IfStatement.cs index f8bebcd0..1231ce4b 100644 --- a/Fluid/Ast/IfStatement.cs +++ b/Fluid/Ast/IfStatement.cs @@ -124,7 +124,7 @@ private async ValueTask Awaited( } else { - await AwaitedElseBranch(new ValueTask(BooleanValue.False), null, writer, encoder, context, startIndex: 0); + await AwaitedElseBranch(new ValueTask(BooleanValue.False), new ValueTask(), writer, encoder, context, startIndex: 0); } return Completion.Normal; diff --git a/Fluid/Ast/TextSpanStatement.cs b/Fluid/Ast/TextSpanStatement.cs index 1dd86125..4bd833b3 100644 --- a/Fluid/Ast/TextSpanStatement.cs +++ b/Fluid/Ast/TextSpanStatement.cs @@ -2,13 +2,14 @@ using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Fluid.Utils; namespace Fluid.Ast { - public class TextSpanStatement : Statement + public sealed class TextSpanStatement : Statement { - private bool _isStripped = false; - private bool _isEmpty = false; + private bool _isStripped; + private bool _isEmpty; private readonly object _synLock = new(); private TextSpan _text; private string _buffer; @@ -33,18 +34,18 @@ public TextSpanStatement(string text) public ref readonly TextSpan Text => ref _text; - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { if (!_isStripped) { StripLeft |= - (PreviousIsTag && context.Options.Trimming.HasFlag(TrimmingFlags.TagRight)) || - (PreviousIsOutput && context.Options.Trimming.HasFlag(TrimmingFlags.OutputRight)) + (PreviousIsTag && (context.Options.Trimming & TrimmingFlags.TagRight) != 0) || + (PreviousIsOutput && (context.Options.Trimming & TrimmingFlags.OutputRight) != 0) ; StripRight |= - (NextIsTag && context.Options.Trimming.HasFlag(TrimmingFlags.TagLeft)) || - (NextIsOutput && context.Options.Trimming.HasFlag(TrimmingFlags.OutputLeft)) + (NextIsTag && (context.Options.Trimming & TrimmingFlags.TagLeft) != 0) || + (NextIsOutput && (context.Options.Trimming & TrimmingFlags.OutputLeft) != 0) ; var span = _text.Buffer; @@ -144,7 +145,7 @@ public override async ValueTask WriteToAsync(TextWriter writer, Text if (_isEmpty) { - return Completion.Normal; + return new ValueTask(Completion.Normal); } context.IncrementSteps(); @@ -152,15 +153,20 @@ public override async ValueTask WriteToAsync(TextWriter writer, Text // The Text fragments are not encoded, but kept as-is // Since WriteAsync needs an actual buffer, we created and reused _buffer - await writer.WriteAsync(_buffer); - //#if NETSTANDARD2_0 - // await writer.WriteAsync(_text.ToString()); - //#else - // await writer.WriteAsync(_text.Span.ToArray()); - //#endif + static async ValueTask Awaited(Task task) + { + await task; + return Completion.Normal; + } + + var task = writer.WriteAsync(_buffer); + if (!task.IsCompletedSuccessfully()) + { + return Awaited(task); + } - return Completion.Normal; + return new ValueTask(Completion.Normal); } } } From e648dd70fb09eb3ca70f2dfae61f72bfad4086db Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 14:46:36 +0300 Subject: [PATCH 10/11] Remove state machines from AssignStatement --- Fluid/Ast/AssignStatement.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Fluid/Ast/AssignStatement.cs b/Fluid/Ast/AssignStatement.cs index 67c88152..a3a119b6 100644 --- a/Fluid/Ast/AssignStatement.cs +++ b/Fluid/Ast/AssignStatement.cs @@ -1,6 +1,7 @@ using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Fluid.Values; namespace Fluid.Ast { @@ -16,14 +17,25 @@ public AssignStatement(string identifier, Expression value) public Expression Value { get; } - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { + static async ValueTask Awaited(ValueTask task, TemplateContext context, string identifier) + { + var value = await task; + context.SetValue(identifier, value); + return Completion.Normal; + } + context.IncrementSteps(); - var value = await Value.EvaluateAsync(context); - context.SetValue(Identifier, value); + var task = Value.EvaluateAsync(context); + if (!task.IsCompletedSuccessfully) + { + return Awaited(task, context, Identifier); + } - return Completion.Normal; + context.SetValue(Identifier, task.Result); + return new ValueTask(Completion.Normal); } } } From cbf7f887ab25c5bea4fe14791aaff770b1e09899 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 14 Apr 2021 19:55:36 +0300 Subject: [PATCH 11/11] make TrimmingFlags a good citizen --- Fluid.Benchmarks/TagBenchmarks.cs | 1 - Fluid/Ast/TextSpanStatement.cs | 9 +++++---- Fluid/TrimmingFlags.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs index 6733685f..952ec761 100644 --- a/Fluid.Benchmarks/TagBenchmarks.cs +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -4,7 +4,6 @@ namespace Fluid.Benchmarks { [MemoryDiagnoser] - [ShortRunJob] public class TagBenchmarks { private static readonly FluidParser _parser = new(); diff --git a/Fluid/Ast/TextSpanStatement.cs b/Fluid/Ast/TextSpanStatement.cs index 4bd833b3..6dff85ab 100644 --- a/Fluid/Ast/TextSpanStatement.cs +++ b/Fluid/Ast/TextSpanStatement.cs @@ -38,14 +38,15 @@ public override ValueTask WriteToAsync(TextWriter writer, TextEncode { if (!_isStripped) { + var trimming = context.Options.Trimming; StripLeft |= - (PreviousIsTag && (context.Options.Trimming & TrimmingFlags.TagRight) != 0) || - (PreviousIsOutput && (context.Options.Trimming & TrimmingFlags.OutputRight) != 0) + (PreviousIsTag && (trimming & TrimmingFlags.TagRight) != 0) || + (PreviousIsOutput && (trimming & TrimmingFlags.OutputRight) != 0) ; StripRight |= - (NextIsTag && (context.Options.Trimming & TrimmingFlags.TagLeft) != 0) || - (NextIsOutput && (context.Options.Trimming & TrimmingFlags.OutputLeft) != 0) + (NextIsTag && (trimming & TrimmingFlags.TagLeft) != 0) || + (NextIsOutput && (trimming & TrimmingFlags.OutputLeft) != 0) ; var span = _text.Buffer; diff --git a/Fluid/TrimmingFlags.cs b/Fluid/TrimmingFlags.cs index cd3537e7..33318b34 100644 --- a/Fluid/TrimmingFlags.cs +++ b/Fluid/TrimmingFlags.cs @@ -8,26 +8,26 @@ public enum TrimmingFlags /// /// Default. Tags and outputs are not trimmed unless the '-' is set on the delimiter. /// - None, - + None = 0, + /// /// Strip blank characters (including , \t, and \r) from the left of tags ({% %}) until \n (exclusive when greedy option os off). /// - TagLeft, + TagLeft = 1, /// /// Strip blank characters (including , \t, and \r) from the right of tags ({% %}) until \n (inclusive when greedy option os off). /// - TagRight, + TagRight = 2, /// /// Strip blank characters (including , \t, and \r) from the left of values ({{ }}) until \n (exclusive when greedy option os off). /// - OutputLeft, + OutputLeft = 4, /// /// Strip blank characters (including , \t, and \r) from the right of values ({{ }}) until \n (inclusive when greedy option os off). /// - OutputRight + OutputRight = 8 } }