Skip to content

Separate parsing from expression binding and expose Expression.Create #577

Description

@Seddryck

Goal

Define the public API for turning Expressif source text into an executable expression while keeping parsing and binding independently accessible and replaceable.

The API should support three usage levels:

// zero-ceremony default
var expression = Expressif.Create("@x | upper");

// explicit stages
var syntax = ExpressionParser.Parse(text);
var expression = ExpressionBinder.Bind(syntax);

// customized construction
var factory = new ExpressionFactory(
    parser: customParser,
    binder: customBinder);

var expression = factory.Create(text);

The design should preserve a simple default API while allowing hosts to replace the parser and/or binder without global mutable configuration.

Public architecture

ExpressionParser
    text -> syntax

ExpressionBinder
    syntax -> IExpression

ExpressionFactory
    configurable composition of parser + binder
    text -> IExpression

Expressif
    static convenience facade using the default ExpressionFactory

Create is not a third interpretation of the language. It is only the composition:

Create(text) = Bind(Parse(text))

ExpressionParser

ExpressionParser belongs to Expressif.Syntax and is responsible only for syntax.

text -> syntax tree

It should:

  • tokenize and parse Expressif source text;
  • produce the canonical syntax/AST model;
  • preserve syntactic structure such as literals, references, function calls, pipelines, arrays, tuples, records, arguments, etc.;
  • report syntax errors and source locations;
  • not resolve runtime function implementations;
  • not create executable expressions;
  • not depend on runtime function catalogs/providers.

Example:

var syntax = ExpressionParser.Parse("@x | upper");

The result is syntax, not an executable expression.

ExpressionBinder

ExpressionBinder is responsible for converting a parsed syntax tree into the runtime expression model.

syntax tree -> bound/executable expression

It should:

  • resolve function names against the available function catalog/providers;
  • bind positional and named arguments to function parameters;
  • bind variables, references, literals, pipelines, and structural expressions to their runtime equivalents;
  • perform semantic validation required for binding;
  • report binding/semantic errors separately from parsing errors;
  • contain no parsing logic.

Example:

var expression = ExpressionBinder.Bind(syntax);

ExpressionFactory

ExpressionFactory is the configurable construction API.

Its public responsibility is:

source/syntax -> IExpression

It composes an IExpressionParser and an IExpressionBinder and lets applications replace either component without modifying global state.

Conceptually:

public sealed class ExpressionFactory
{
    public ExpressionFactory(
        IExpressionParser parser,
        IExpressionBinder binder)
    {
        Parser = parser;
        Binder = binder;
    }

    public IExpressionParser Parser { get; }
    public IExpressionBinder Binder { get; }

    public IExpression Create(string text)
        => Create(Parser.Parse(text));

    public IExpression Create(ExpressionSyntax syntax)
        => Binder.Bind(syntax);
}

The exact interface and constructor shape may differ, but the responsibility boundary should remain the same.

Typical customization:

var factory = new ExpressionFactory(
    parser: customParser,
    binder: customBinder);

var expression = factory.Create(text);

Replacing only one component should be possible without unnecessary ceremony, for example by using the standard implementation for the other component.

ExpressionFactory should not expose its own parsing or binding semantics. It delegates to its configured parser and binder.

Expressif

Expressif is the zero-ceremony static facade for consumers that want the standard configuration.

The common case remains:

var expression = Expressif.Create("@x | upper");

Conceptually:

public static class Expressif
{
    private static readonly ExpressionFactory DefaultFactory = ...;

    public static IExpression Create(string text)
        => DefaultFactory.Create(text);
}

Expressif must not expose mutable global parser/binder configuration such as:

Expressif.Parser = customParser;
Expressif.Binder = customBinder;

Customization belongs to ExpressionFactory instances so tests and concurrent consumers remain isolated.

Usage model

Common case

var expression = Expressif.Create(text);

Explicit parsing and binding

var syntax = ExpressionParser.Parse(text);
var expression = ExpressionBinder.Bind(syntax);

Custom parser and/or binder

var factory = new ExpressionFactory(
    parser: customParser,
    binder: customBinder);

var expression = factory.Create(text);

The same factory can also operate on an already parsed syntax tree:

var expression = factory.Create(syntax);

Why this separation

This keeps the layers usable independently while preserving a small public surface:

  • Expressif.Syntax and the Language Server can parse source code without loading or executing runtime functions;
  • tooling can inspect syntax even when binding is impossible or incomplete;
  • the runtime can bind an already parsed syntax tree without reparsing it;
  • syntax errors and semantic/binding errors remain distinct;
  • applications can replace the parser or binder through an ExpressionFactory instance;
  • no global mutable configuration is required;
  • the common case remains one call: Expressif.Create(text).

The terminology should remain consistent:

Parse  = text -> syntax
Bind   = syntax -> executable expression
Create = source/syntax -> executable expression
         (factory operation; parse + bind when source text is supplied)

Acceptance criteria

  • Introduce ExpressionBinder.Bind(...) as the canonical syntax-to-runtime entry point.
  • Parsing remains exclusively owned by ExpressionParser / Expressif.Syntax.
  • ExpressionFactory is the configurable composition point for parser and binder.
  • ExpressionFactory.Create(string) parses through its configured parser and binds through its configured binder.
  • ExpressionFactory.Create(ExpressionSyntax) binds an already parsed syntax tree without reparsing it.
  • A consumer can replace the parser, binder, or both through an ExpressionFactory instance.
  • Add Expressif.Create(string) as the public zero-ceremony API using the default factory.
  • Expressif.Parse(...) and Expressif.Bind(...) may expose the default lower-level stages directly.
  • No mutable global parser/binder configuration is introduced.
  • No parsing logic exists in ExpressionBinder.
  • No runtime binding/function-resolution logic exists in ExpressionParser.
  • ExpressionFactory does not duplicate parsing or binding logic.
  • Parsing and binding failures remain distinguishable.
  • Add tests covering the explicit two-step API, the configurable factory API, and the Expressif.Create(string) convenience API.

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