SELSimple Expression Language
GitHub

Using SEL

SEL is embedded: your application hands a rule and some data to the host library, and gets a value back. This page is that host API in all five languages. The pages after it are complete programs, each written five times:

Page What it shows
A REPL in thirty lines the whole API in the smallest useful program
Validation a form's rule set: compiled once, run per submission, errors told apart
Scripting with host functions the application registers functions; a script decides what happens
SQL conditions a rule as a WHERE clause — naive, and with the schema described
SQL pipelines whole queries in SQL, split with memory, or in memory — star, EAV, 3NF, flat and complex data
Your own functions, in SQL host functions with a SQL spelling: PostgreSQL SQL and PL/pgSQL functions, list arguments, strict mode

Every snippet on these pages is quoted from a file under examples/ that the test suite runs in all five languages and whose output it compares byte for byte — the code cannot drift from what the page says, and the five tabs cannot drift from each other.


Installing

The package is sel-lang everywhere. Python, PHP and JavaScript have no dependencies, so copying the directory works just as well as a package manager.

sh
pip install sel-lang                 # or copy python/sel/ into your project
python
from sel import compile, evaluate, Value, SelError      # Python 3.10+
from sel.sql import Sql, Binding                        # the SQL layer, when you need it

PACKAGING.md has the details of each registry.

Evaluate, or compile once and run many times

evaluate(source, context) parses and runs in one call. A form or a batch job compiles each rule once and keeps the program: a Program is immutable and reusable, and compiling is where syntax errors, unknown functions and wrong argument counts are caught — before any data is involved.

python
rule = compile('IF(QTY * PRICE > LIMIT, "over budget", "ok")')
for row in [{'QTY': '3', 'PRICE': '19.99'}, {'QTY': '1', 'PRICE': '5.00'}]:
    ctx = Value.from_native({**row, 'LIMIT': '50.00'})
    print(f"   QTY={row['QTY']} PRICE={row['PRICE']} =>", rule.run(ctx).as_text())

Money is text. Pass "19.99", never a float: a double has already lost the exactness SEL exists to keep, and the dynamic hosts refuse one rather than pretend otherwise.

Building a context

The context is a value whose children are the rule's variables. Each host builds it from its own maps and lists; C++, which has no native map to convert from, builds it with Value::none() and set().

python
order = Value.from_native({
    'CUSTOMER': 'Zażółć',
    'ITEMS': [                                    # a list is a 1-based SEL list
        {'SKU': 'AB-1234', 'QTY': '3', 'PRICE': '19.99'},
        {'SKU': 'CD-5678', 'QTY': '1', 'PRICE': '5.01'},
    ],
})
print('   first SKU =>', compile('ITEMS[1]["SKU"]').run(order).as_text())
print('   total     =>', compile('SUM(ITEMS, _["QTY"] * _["PRICE"])').run(order).as_text())
print('   0.10+0.20 =>', evaluate('0.10 + 0.20').as_text())

Variables flow back

The context is changed in place, so a rule can hand back more than its result:

python
ctx = Value.from_native({'QTY': '3', 'PRICE': '19.99'})
compile('NET = QTY * PRICE; VAT = ROUND(NET * 0.23, 2); GROSS = NET + VAT').run(ctx)
for name in ['NET', 'VAT', 'GROSS']:
    print(f'   {name:<5} =>', ctx.get(name).as_text())

Errors

Every failure is one exception type with a stable code and the position of the node that failed. Match on the code; the message is for people. E_ABORT is the rule speaking — everything else is the rule failing.

python
for src in ['3 + "A"', 'NOSUCH(1)', 'IF(1, "a", "b")', 'ABORT("no stock")']:
    try:
        evaluate(src)
        print(f'   {src:<17} => no error')
    except SelError as e:
        print(f'   {src:<17} => {e.code} at {e.line}:{e.col}')

What does a rule read?

dependencies() answers statically, without running the rule — which is what lets a form re-check only the rules a changed field can affect:

python
print('  ', ' '.join(
    compile('T = SUM(ITEMS, _["QTY"]); T > LIMIT AND CUSTOMER $!= ""').dependencies()))

The API side by side

Python JavaScript PHP C++ Common Lisp
compile compile(src) compile(src) Sel::compile($src) sel::compile(src) (sel:compile-source src)
run p.run(ctx) p.run(ctx) $p->run($ctx) p.run(ctx) (sel:run p ctx)
evaluate evaluate(src, ctx) evaluate(src, ctx) Sel::evaluate($src, $ctx) sel::evaluate(src, ctx) (sel:evaluate src ctx)
inputs p.dependencies() p.dependencies() $p->dependencies() p.dependencies() (sel:dependencies p)
context Value.from_native({...}) Value.fromNative({...}) Value::fromNative([...]) Value::none() + set (sel:from-native ...)
result .as_text() .as_bool() .dump() .asText() .asBool() .dump() ->asText() ->asBool() ->dump() .as_text() .as_bool() .dump() (sel:as-text v) (sel:as-bool v) (sel:value-dump v)
errors SelError .code .line .col SelError .code .line .col SelError ->code ->line ->col sel::SelError .code() .line() .col() sel:sel-error sel-error-code …
own functions register_function registerFunction Sel::registerFunction sel::register_function sel:register-function

The full contract is spec/SPEC.md §8, and tools/check-api.sh runs the same probes through all five bindings to keep the answers identical.

View this page's Markdown on GitHub