diff --git a/Fluid.Benchmarks/TagBenchmarks.cs b/Fluid.Benchmarks/TagBenchmarks.cs new file mode 100644 index 00000000..952ec761 --- /dev/null +++ b/Fluid.Benchmarks/TagBenchmarks.cs @@ -0,0 +1,134 @@ +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; + private readonly TestCase _assign; + private readonly TestCase _else; + private readonly TestCase _textSpan; + + 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 %}"); + _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(); + } + + [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); + } + + [Benchmark] + public object Assign_Parse() + { + return _assign.Parse(); + } + + [Benchmark] + 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); + } + + [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; + 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 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); } } } 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..56227201 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++) + for (var i = 0; i < _statements.Count; i++) { context.IncrementSteps(); - var completion = await _statements[index].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; + } + // 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/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 3c334a6b..1231ce4b 100644 --- a/Fluid/Ast/IfStatement.cs +++ b/Fluid/Ast/IfStatement.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Fluid.Values; namespace Fluid.Ast { @@ -27,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); @@ -50,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), new ValueTask(), 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; } } 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 4c7ab329..8d7e5ca6 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) { - context.IncrementSteps(); + static async ValueTask Awaited(Task task) + { + await task; + return Completion.Normal; + } -#if NETSTANDARD2_0 - await writer.WriteAsync(_text.ToString()); -#else - await writer.WriteAsync(_text.Span.ToArray()); -#endif + context.IncrementSteps(); - 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/Ast/TextSpanStatement.cs b/Fluid/Ast/TextSpanStatement.cs index 1dd86125..6dff85ab 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,19 @@ 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) { + var trimming = context.Options.Trimming; StripLeft |= - (PreviousIsTag && context.Options.Trimming.HasFlag(TrimmingFlags.TagRight)) || - (PreviousIsOutput && context.Options.Trimming.HasFlag(TrimmingFlags.OutputRight)) + (PreviousIsTag && (trimming & TrimmingFlags.TagRight) != 0) || + (PreviousIsOutput && (trimming & TrimmingFlags.OutputRight) != 0) ; StripRight |= - (NextIsTag && context.Options.Trimming.HasFlag(TrimmingFlags.TagLeft)) || - (NextIsOutput && context.Options.Trimming.HasFlag(TrimmingFlags.OutputLeft)) + (NextIsTag && (trimming & TrimmingFlags.TagLeft) != 0) || + (NextIsOutput && (trimming & TrimmingFlags.OutputLeft) != 0) ; var span = _text.Buffer; @@ -144,7 +146,7 @@ public override async ValueTask WriteToAsync(TextWriter writer, Text if (_isEmpty) { - return Completion.Normal; + return new ValueTask(Completion.Normal); } context.IncrementSteps(); @@ -152,15 +154,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); } } } 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/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 } } 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