Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .github/skills/scaffold/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ Where:

Run all checks before proposing a record:
- `name` is non-empty, lowercase kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`).
- Predicate names read naturally as boolean questions: use a third-person singular verb (for example, `contains`, `starts-with`, `ends-with`, `matches`, or `exists`) when the name is verbal; otherwise prefix the name with `is-` or `has-` as appropriate.
- Reject predicate names that use an infinitive/base verb form (for example, `contain`, `start-with`, `end-with`, `match`, or `exist`) or a non-verbal name without an `is-`/`has-` prefix.
- Logical combinator names (`and`, `or`, `xor`, and `not`) are operator-style exceptions and must not receive an `is-` prefix.
- `scope` is exactly one of: `Text`, `Numeric`, `Temporal`, `Special`, `Array`.
- `summary` is non-empty after normalization.
- Parameter names are unique and kebab-case.
Expand Down
42 changes: 42 additions & 0 deletions Expressif.Cli.Tests/BindCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,48 @@ public async Task Bind_ValidExpression_DefaultsToHumanReadableTree()
});
}

[Test]
public async Task Bind_PredicateOnlyExpression_IsValid()
{
var result = await InvokeAsync("bind", "even");

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut, Does.Contain("Function: even"));
Assert.That(result.StdErr, Is.Empty);
});
}

[Test]
public async Task Bind_BinaryPredicate_PreservesBothOperands()
{
var result = await InvokeAsync("bind", "even |AND greater-than(5)");

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut, Does.Contain("Function: and"));
Assert.That(result.StdOut, Does.Contain("Arg[0]: OpenExpression"));
Assert.That(result.StdOut, Does.Contain("Arg[1]: OpenExpression"));
Assert.That(result.StdErr, Is.Empty);
});
}

[Test]
public async Task Bind_ClosedFunctionPredicateArgument_ResolvesNestedFunction()
{
var result = await InvokeAsync("bind", "even |or greater-than(45 | add(17))");

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut, Does.Contain("Arg[0]: InputExpression"));
Assert.That(result.StdOut, Does.Contain("Function: add"));
Assert.That(result.StdErr, Is.Empty);
});
}

[Test]
public async Task Bind_FieldShorthand_ShowsBinderGeneratedFunction()
{
Expand Down
87 changes: 87 additions & 0 deletions Expressif.Cli.Tests/CliCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,93 @@ public async Task Evaluate_ValidExpression_ReturnsResultAndSuccessCode()
});
}

[TestCase("even", "4", "true")]
[TestCase("even", "5", "false")]
[TestCase("is-even", "4", "true")]
public async Task Evaluate_PredicateOnlyExpression_ReturnsBoolean(string expression, string input, string expected)
{
var result = await InvokeAsync("evaluate", expression, "--input", input);

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut.Trim(), Is.EqualTo(expected));
Assert.That(result.StdErr, Is.Empty);
});
}

[TestCase("4", "false")]
[TestCase("6", "true")]
[TestCase("7", "false")]
public async Task Evaluate_ComposedPredicateOnlyExpression_ReturnsBoolean(string input, string expected)
{
var result = await InvokeAsync("evaluate", "even |AND greater-than(5)", "--input", input);

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut.Trim(), Is.EqualTo(expected));
Assert.That(result.StdErr, Is.Empty);
});
}

[TestCase("-1", "true")]
[TestCase("6", "true")]
[TestCase("3", "false")]
[TestCase("5", "false")]
public async Task Evaluate_NestedLowercasePredicateExpression_ReturnsBoolean(string input, string expected)
{
var result = await InvokeAsync(
"evaluate",
"(even |and greater-than(5)) |or less-than(0)",
"--input",
input);

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut.Trim(), Is.EqualTo(expected));
Assert.That(result.StdErr, Is.Empty);
});
}

[TestCase("-3", "true")]
[TestCase("-2", "false")]
[TestCase("6", "true")]
[TestCase("7", "false")]
public async Task Evaluate_TwoNestedPredicateBranches_ReturnsBoolean(string input, string expected)
{
var result = await InvokeAsync(
"evaluate",
"(odd |and less-than(0)) |or (even |and greater-than(5))",
"--input",
input);

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut.Trim(), Is.EqualTo(expected));
Assert.That(result.StdErr, Is.Empty);
});
}

[Test]
public async Task Evaluate_PredicateClosedInputExpression_ResolvesNestedFunction()
{
var result = await InvokeAsync(
"evaluate",
"filter(greater-than(17 | add(17)))",
"--input",
"{10,12,13}");

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut.Trim(), Is.EqualTo("{}"));
Assert.That(result.StdErr, Is.Empty);
});
}

[Test]
public async Task Evaluate_MultipleInputOptions_ReturnsClearError()
{
Expand Down
13 changes: 13 additions & 0 deletions Expressif.Cli.Tests/ParseCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ public async Task Parse_ValidExpression_DefaultsToHumanReadableTree()
});
}

[Test]
public async Task Parse_BinaryPredicate_DisplaysNormalizedOperator()
{
var result = await InvokeAsync("parse", "even |AND greater-than(5)");

Assert.Multiple(() =>
{
Assert.That(result.ExitCode, Is.EqualTo(ExitCodes.Success));
Assert.That(result.StdOut, Does.Contain("BinaryOperator: AND"));
Assert.That(result.StdErr, Is.Empty);
});
}

[Test]
public async Task Parse_JsonOutput_IsMachineReadable()
{
Expand Down
1 change: 1 addition & 0 deletions Expressif.Cli/Commands/SyntaxTreeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ private static string GetLabel(SyntaxNode node)
var value = node switch
{
FunctionCallSyntax function => function.Name,
BinaryOperatorSyntax binary => binary.Text.TrimStart('|').ToUpperInvariant(),
NumericLiteralSyntax numeric => numeric.Value.ToString(CultureInfo.InvariantCulture),
BooleanLiteralSyntax boolean => boolean.Value.ToString().ToLowerInvariant(),
NullLiteralSyntax => "null",
Expand Down
11 changes: 6 additions & 5 deletions Expressif.Testing/Bindings/FunctionBinderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,16 @@ public void Parse_Function_FilterWithAndPredicate_Valid()
Assert.That(function.Parameters[0], Is.TypeOf<OpenExpressionParameter>());

var parameter = (OpenExpressionParameter)function.Parameters[0];
var leftFunction = parameter.Expression.Members.First();
var combinator = parameter.Expression.Members.Single();
Assert.That(combinator.Name, Is.EqualTo("and"));
Assert.That(combinator.Parameters, Has.Length.EqualTo(2));

var leftFunction = ((OpenExpressionParameter)combinator.Parameters[0]).Expression.Members.Single();
Assert.That(leftFunction.Name, Is.EqualTo("greater-than"));
Assert.That(leftFunction.Parameters, Has.Length.EqualTo(1));
Assert.That(((LiteralParameter)leftFunction.Parameters[0]).Value, Is.EqualTo(2m));

var combinator = parameter.Expression.Members.Last();
Assert.That(combinator.Name, Is.EqualTo("and"));
Assert.That(combinator.Parameters.Single(), Is.TypeOf<OpenExpressionParameter>());
var rightFunction = ((OpenExpressionParameter)combinator.Parameters.Single()).Expression.Members.Single();
var rightFunction = ((OpenExpressionParameter)combinator.Parameters[1]).Expression.Members.Single();
Assert.That(rightFunction.Name, Is.EqualTo("less-than"));
Assert.That(rightFunction.Parameters, Has.Length.EqualTo(1));
Assert.That(((LiteralParameter)rightFunction.Parameters[0]).Value, Is.EqualTo(5m));
Expand Down
19 changes: 15 additions & 4 deletions Expressif.Testing/Bindings/PredicationBinderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,26 @@ public void Parse_UnaryPredication_Valid(string value)
public void Parse_BinaryPredication_Valid(string value)
=> Assert.That(BindingTestAdapter.Predication(value), Is.Not.Null);

[TestCase("!even", "not")]
public void Parse_NotShorthand_LowersToCombinator()
{
var predication = (SinglePredication)BindingTestAdapter.Predication("!even");

Assert.That(predication.Members.Last().Name, Is.EqualTo("not"));
}

[TestCase("even |AND odd", "and")]
[TestCase("even |OR odd", "or")]
[TestCase("even |XOR odd", "xor")]
public void Parse_BooleanShorthand_LowersToCombinator(string value, string expected)
public void Parse_BinaryShorthand_PreservesBothOperands(string value, string expected)
{
var predication = (SinglePredication)BindingTestAdapter.Predication(value);
var predication = (BinaryPredication)BindingTestAdapter.Predication(value);

Assert.That(predication.Members.Last().Name, Is.EqualTo(expected));
Assert.Multiple(() =>
{
Assert.That(predication.Operator.Name, Is.EqualTo(expected));
Assert.That(predication.LeftMember, Is.TypeOf<SinglePredication>());
Assert.That(predication.RightMember, Is.TypeOf<SinglePredication>());
});
}

[Test]
Expand Down
3 changes: 3 additions & 0 deletions Expressif.Testing/Expressif.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@
<ItemGroup>
<ProjectReference Include="..\Expressif\Expressif.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="..\predicates-rename.json" Link="predicates-rename.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
30 changes: 30 additions & 0 deletions Expressif.Testing/Functions/FunctionFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ namespace Expressif.Testing.Functions;

public class FunctionFactoryTest
{
[TestCase("even", 4, true)]
[TestCase("even", 5, false)]
[TestCase("is-even", 4, true)]
public void Instantiate_PredicateOnlyExpression_EvaluatesBoolean(string source, object value, bool expected)
=> Assert.That(BindingTestAdapter.Executable(source).Evaluate(value), Is.EqualTo(expected));

[TestCase(4, false)]
[TestCase(6, true)]
[TestCase(7, false)]
public void Instantiate_ComposedPredicateOnlyExpression_EvaluatesAgainstOriginalInput(object value, bool expected)
=> Assert.That(BindingTestAdapter.Executable("even |AND greater-than(5)").Evaluate(value), Is.EqualTo(expected));

[TestCase("replace-slice(2, 4, \"abc\")")]
[TestCase("replace-slice(start := 2, length := 4, append := \"abc\")")]
[TestCase("replace-slice(2, append := \"abc\", length := 4)")]
Expand Down Expand Up @@ -238,6 +250,24 @@ public void Instantiate_FilterWithPredicateExpression_Valid()
Assert.That(predicate, Is.TypeOf<GreaterThan>());
}

[Test]
public void Instantiate_FilterWithClosedPredicateParameter_ResolvesNestedFunction()
{
var reference = new InputExpressionParameter(
new Expressif.Bindings.ClosedExpression(
new LiteralParameter(17m),
[new Function("add", [new LiteralParameter(17m)])]));
var predicate = new Function("greater-than", [reference]);
var filter = new Function(
"filter",
[new OpenExpressionParameter(new OpenExpression([predicate]))]);
var root = new OpenRootExpression(new OpenExpression([filter]));

var function = new FunctionFactory().Instantiate(root, new Context());

Assert.That(function.Evaluate(new object[] { 10, 12, 13 }), Is.Empty);
}

[Test]
public void Instantiate_SliceAlias_Valid()
{
Expand Down
4 changes: 2 additions & 2 deletions Expressif.Testing/Predicates/Boolean/IdenticalToTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Expressif.Predicates.Boolean;
using Expressif.Predicates.Boolean;
using Expressif.Testing.Conformance;

namespace Expressif.Testing.Predicates.Boolean;
Expand All @@ -7,7 +7,7 @@ namespace Expressif.Testing.Predicates.Boolean;
public class IdenticalToTest
{
[Conformance]
public void IdenticalTo_Valid(object? value, bool reference, bool expected)
public void IsIdenticalTo_Valid(object? value, bool reference, bool expected)
=> Assert.That(new IdenticalTo(() => reference).Evaluate(value), Is.EqualTo(expected));
}

10 changes: 5 additions & 5 deletions Expressif.Testing/Predicates/Boolean/TrueFalseTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Expressif.Predicates.Boolean;
using Expressif.Predicates.Boolean;
using Expressif.Testing.Conformance;

namespace Expressif.Testing.Predicates.Boolean;
Expand All @@ -7,18 +7,18 @@ namespace Expressif.Testing.Predicates.Boolean;
public class TrueFalseTest
{
[Conformance]
public void True_Valid(object? value, bool expected)
public void IsTrue_Valid(object? value, bool expected)
=> Assert.That(new True().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void TrueOrNull_Valid(object? value, bool expected)
public void IsTrueOrNull_Valid(object? value, bool expected)
=> Assert.That(new TrueOrNull().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void False_Valid(object? value, bool expected)
public void IsFalse_Valid(object? value, bool expected)
=> Assert.That(new False().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void FalseOrNull_Valid(object? value, bool expected)
public void IsFalseOrNull_Valid(object? value, bool expected)
=> Assert.That(new FalseOrNull().Evaluate(value), Is.EqualTo(expected));
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@ public void Locate_ExpressifAssembly_ElementsReturned()
}

[Test]
public void Locate_ExpressifAssembly_NameEqualClass()
public void Locate_ExpressifAssembly_NameFollowsPredicateConvention()
{
foreach (var info in Infos)
{
Debug.WriteLine(info.Name);
Assert.That(info.Name, Is.Not.Null.And.Not.Empty);
Assert.That(info.Name.ToPascalCase(), Is.EqualTo(info.ImplementationType.Name));
Assert.That(
info.Name.StartsWith("is-", StringComparison.Ordinal)
|| info.Name.StartsWith("has-", StringComparison.Ordinal)
|| info.Name is "and" or "or" or "xor" or "not"
|| info.Name is "contains" or "starts-with" or "ends-with"
|| info.Name.StartsWith("matches-", StringComparison.Ordinal),
Is.True,
$"Predicate '{info.Name}' does not follow the naming convention.");
}
}

Expand Down
16 changes: 8 additions & 8 deletions Expressif.Testing/Predicates/Numeric/ComparisonShortcutTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Expressif.Predicates.Numeric;
using Expressif.Predicates.Numeric;
using Expressif.Testing.Conformance;

namespace Expressif.Testing.Predicates.Numeric;
Expand All @@ -7,30 +7,30 @@ namespace Expressif.Testing.Predicates.Numeric;
public class ComparisonShortcutTest
{
[Conformance]
public void Zero_Valid(object? value, bool expected)
public void IsZero_Valid(object? value, bool expected)
=> Assert.That(new Zero().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void ZeroOrNull_Valid(object? value, bool expected)
public void IsZeroOrNull_Valid(object? value, bool expected)
=> Assert.That(new ZeroOrNull().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void One_Valid(object? value, bool expected)
public void IsOne_Valid(object? value, bool expected)
=> Assert.That(new One().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void Positive_Valid(object? value, bool expected)
public void IsPositive_Valid(object? value, bool expected)
=> Assert.That(new Positive().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void PositiveOrZero_Valid(object? value, bool expected)
public void IsPositiveOrZero_Valid(object? value, bool expected)
=> Assert.That(new PositiveOrZero().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void Negative_Valid(object? value, bool expected)
public void IsNegative_Valid(object? value, bool expected)
=> Assert.That(new Negative().Evaluate(value), Is.EqualTo(expected));

[Conformance]
public void NegativeOrZero_Valid(object? value, bool expected)
public void IsNegativeOrZero_Valid(object? value, bool expected)
=> Assert.That(new NegativeOrZero().Evaluate(value), Is.EqualTo(expected));
}
Loading
Loading