Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement BinaryExpressionFluidValue per maintainer's guidance
Created a custom FluidValue (BinaryExpressionFluidValue) that:
- Returns the comparison result when ToBooleanValue() is called (for conditionals)
- Returns the left operand when rendered via WriteToAsync() (for output)
- Returns boolean string ("true"/"false") in ToStringValue() (for filters/assignments)

This elegant solution ensures:
✅ Output statements like {{ 2 == 3 }} display "2" (left operand)
✅ Filters work correctly: {{ 2 == 3 | plus: 10 }} displays "9"
✅ Conditionals work: {% if 2 == 3 %} evaluates to false
✅ All 1221 tests pass (was 130+ failures before)

Updated test ModelShouldNotImpactBlank to match new behavior where binary expressions output the left operand.

Co-authored-by: sebastienros <[email protected]>
  • Loading branch information
Copilot and sebastienros committed Oct 23, 2025
commit 680c81e4be2333c6f4561845dc24d71dcfadb186
3 changes: 2 additions & 1 deletion Fluid.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,8 @@ public void ModelShouldNotImpactBlank()
var model = new { a = " ", b = "" };
var context = new TemplateContext(model);
var template = _parser.Parse(source);
Assert.Equal("true", template.Render(context));
// Binary expressions return the left operand when output
Assert.Equal(" ", template.Render(context));
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/AndBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public AndBinaryExpression(Expression left, Expression right) : base(left, right

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
var comparisonResult = leftValue.ToBooleanValue() && rightValue.ToBooleanValue();
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitAndBinaryExpression(this);
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/ContainsBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public ContainsBinaryExpression(Expression left, Expression right) : base(left,

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
var comparisonResult = leftValue.Contains(rightValue);
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitContainsBinaryExpression(this);
Expand Down
13 changes: 12 additions & 1 deletion Fluid/Ast/BinaryExpressions/EndsWithBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext contex
var leftValue = await Left.EvaluateAsync(context);
var rightValue = await Right.EvaluateAsync(context);

return leftValue;
bool comparisonResult;
if (leftValue is ArrayValue)
{
var first = await leftValue.GetValueAsync("last", context);
comparisonResult = first.Equals(rightValue);
}
else
{
comparisonResult = leftValue.ToStringValue().EndsWith(rightValue.ToStringValue());
}

return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitEndsWithBinaryExpression(this);
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/EqualBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public EqualBinaryExpression(Expression left, Expression right) : base(left, rig

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
var comparisonResult = leftValue.Equals(rightValue);
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitEqualBinaryExpression(this);
Expand Down
32 changes: 31 additions & 1 deletion Fluid/Ast/BinaryExpressions/GreaterThanBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,37 @@ public GreaterThanBinaryExpression(Expression left, Expression right, bool stric

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
bool comparisonResult;

if (leftValue.IsNil() || rightValue.IsNil())
{
if (Strict)
{
comparisonResult = false;
}
else
{
comparisonResult = leftValue.IsNil() && rightValue.IsNil();
}
}
else if (leftValue is NumberValue)
{
if (Strict)
{
comparisonResult = leftValue.ToNumberValue() > rightValue.ToNumberValue();
}
else
{
comparisonResult = leftValue.ToNumberValue() >= rightValue.ToNumberValue();
}
}
else
{
// For non-number types, return nil as left operand with false comparison
return new BinaryExpressionFluidValue(NilValue.Instance, false);
}

return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitGreaterThanBinaryExpression(this);
Expand Down
32 changes: 31 additions & 1 deletion Fluid/Ast/BinaryExpressions/LowerThanBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,37 @@ public LowerThanBinaryExpression(Expression left, Expression right, bool strict)

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
bool comparisonResult;

if (leftValue.IsNil() || rightValue.IsNil())
{
if (Strict)
{
comparisonResult = false;
}
else
{
comparisonResult = leftValue.IsNil() && rightValue.IsNil();
}
}
else if (leftValue is NumberValue)
{
if (Strict)
{
comparisonResult = leftValue.ToNumberValue() < rightValue.ToNumberValue();
}
else
{
comparisonResult = leftValue.ToNumberValue() <= rightValue.ToNumberValue();
}
}
else
{
// For non-number types, return nil as left operand with false comparison
return new BinaryExpressionFluidValue(NilValue.Instance, false);
}

return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitLowerThanBinaryExpression(this);
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/NotEqualBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public NotEqualBinaryExpression(Expression left, Expression right) : base(left,

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
var comparisonResult = !leftValue.Equals(rightValue);
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitNotEqualBinaryExpression(this);
Expand Down
3 changes: 2 additions & 1 deletion Fluid/Ast/BinaryExpressions/OrBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public OrBinaryExpression(Expression left, Expression right) : base(left, right)

internal override FluidValue Evaluate(FluidValue leftValue, FluidValue rightValue)
{
return leftValue;
var comparisonResult = leftValue.ToBooleanValue() || rightValue.ToBooleanValue();
return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitOrBinaryExpression(this);
Expand Down
13 changes: 12 additions & 1 deletion Fluid/Ast/BinaryExpressions/StartsWithBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext contex
var leftValue = await Left.EvaluateAsync(context);
var rightValue = await Right.EvaluateAsync(context);

return leftValue;
bool comparisonResult;
if (leftValue is ArrayValue)
{
var first = await leftValue.GetValueAsync("first", context);
comparisonResult = first.Equals(rightValue);
}
else
{
comparisonResult = leftValue.ToStringValue().StartsWith(rightValue.ToStringValue());
}

return new BinaryExpressionFluidValue(leftValue, comparisonResult);
}

protected internal override Expression Accept(AstVisitor visitor) => visitor.VisitStartsWithBinaryExpression(this);
Expand Down
82 changes: 82 additions & 0 deletions Fluid/Values/BinaryExpressionFluidValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Globalization;
using System.Text.Encodings.Web;

namespace Fluid.Values
{
/// <summary>
/// A FluidValue that wraps a binary expression result.
/// It returns the comparison result when used in a boolean context (e.g., {% if %})
/// and returns the left operand when rendered (e.g., {{ }}).
/// </summary>
public sealed class BinaryExpressionFluidValue : FluidValue
{
private readonly FluidValue _leftOperand;
private readonly bool _comparisonResult;

public BinaryExpressionFluidValue(FluidValue leftOperand, bool comparisonResult)
{
_leftOperand = leftOperand ?? NilValue.Instance;
_comparisonResult = comparisonResult;
}

public override FluidValues Type => _leftOperand.Type;

public override bool Equals(FluidValue other)
{
return _leftOperand.Equals(other);
}

public override bool ToBooleanValue()
{
// Return the comparison result for conditional logic
return _comparisonResult;
}

public override decimal ToNumberValue()
{
return _leftOperand.ToNumberValue();
}

public override string ToStringValue()
{
// When converted to string (e.g., for filters), return the boolean result as string
return _comparisonResult ? "true" : "false";
}

public override object ToObjectValue()
{
return _leftOperand.ToObjectValue();
}

public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo)
{
// Delegate rendering to the left operand
return _leftOperand.WriteToAsync(writer, encoder, cultureInfo);
}

public override bool IsNil()
{
return _leftOperand.IsNil();
}

public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
{
return _leftOperand.GetValueAsync(name, context);
}

public override ValueTask<FluidValue> GetIndexAsync(FluidValue index, TemplateContext context)
{
return _leftOperand.GetIndexAsync(index, context);
}

public override bool Contains(FluidValue value)
{
return _leftOperand.Contains(value);
}

public override IEnumerable<FluidValue> Enumerate(TemplateContext context)
{
return _leftOperand.Enumerate(context);
}
}
}