Skip to content

Treat predicates as Boolean-returning functions in expression pipelines #662

Description

@Seddryck

Context

Predicates implement IFunction and should therefore participate in ordinary expression pipelines as Boolean-returning functions. Binding currently treats functions and predicates as separate categories in several paths, which produces inconsistent behavior and duplicated adapters.

For example:

filter(equal-to(2024))

works, while:

filter(.date | year | equal-to(2024))

fails with Unknown function 'equal-to'. Once field shorthand is present, the filter-specific binder sends every pipeline member through function-only resolution and never resolves the terminal predicate.

The same model should support predicates as ordinary Boolean-returning functions outside filter, including:

is-null |or even
year | equal-to(2024)
.date | year | equal-to(2024)

Findings

  • ExpressionFactory has separate function and predicate resolution paths even though IPredicate extends IFunction.
  • Transformation binding contains a predicate fallback, but normal root pipelines and the field-shorthand filter path do not consistently use it.
  • BuildSinglePredicateFromOpenExpression special-cases field shorthand and constructs an ExpressionFactory for every member, causing predicate names to be resolved as ordinary functions.
  • TryBuildBinaryCallable duplicates the same function-then-predicate lookup policy.
  • filter must remain strict: its argument must be a Boolean-returning function expression, not an arbitrary function.
  • Boolean composition should follow the same function model. !, |AND, |OR, and |XOR are shorthand for Boolean functions such as not, and, or, and xor; their operands are evaluated against the appropriate shared input and must return Boolean values.
  • SinglePredication stores an array of members, although its parser creates one member and PredicationFactory returns only the first instantiated predicate. This silently discards any additional members and obscures its intended role.
  • BooleanExpressionPredicate is an adapter created by the split binding model and should be reviewed for removal once Boolean-returning pipelines are bound consistently.

Proposed refactoring

  1. Introduce one callable-resolution path that can bind ordinary functions and predicates as IFunction implementations.
  2. Preserve an explicit Boolean output contract so Boolean consumers can reject non-Boolean expressions.
  3. Route root expressions, nested transformation expressions, field shorthand, tuple shorthand, map, and filter through the shared pipeline-member binder.
  4. Bind filter arguments as Boolean-returning function expressions and reject pipelines whose result is not Boolean.
  5. Lower or bind !, |AND, |OR, and |XOR as Boolean function composition rather than maintaining a separate execution model.
  6. Make SinglePredication genuinely singular or remove it if logical shorthand can be represented directly by the unified expression AST.
  7. Remove duplicated predicate fallbacks and field-specific Boolean adapters after the shared binder is in place.
  8. Define a deterministic validation rule for public names exposed by both the function and predicate registries.

Acceptance criteria

  • Pure predicates can be evaluated as ordinary open expressions.
  • Pipelines can mix transformations and Boolean-returning predicate functions.
  • is-null |or even works without being wrapped in filter.
  • filter(equal-to(2024)) continues to work.
  • filter(is-null |or even) works.
  • filter(.date | year | equal-to(2024)) returns the matching records.
  • map(even) continues to return Boolean values while preserving cardinality.
  • filter rejects a non-Boolean terminal function such as year or add(1) with a clear diagnostic.
  • Logical operands that do not evaluate to Boolean are rejected rather than accepted through undocumented truthiness.
  • SinglePredication cannot contain silently ignored members.
  • Existing predicate grouping, negation, and logical composition behavior remains covered.
  • Function/predicate name collision behavior is covered by tests.
  • The solution builds and all relevant unit and conformance tests pass.

Dependency and delivery

Implement this work on top of #661 because it extends the binding and implicit pipeline coercion work introduced there. Open the implementation pull request against next-major.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    code-refactoringNo change from a functional perspective but improve code performances or maintainabilityenhancementEnhancement to an existing feature

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions