Skip to main content

SEL — Simple Expression Language

Write a business rule once. Run it in five languages — or in your database — and get the same answer.

Python · JavaScript · PHP · C++23 · Common Lisp  |  MariaDB · MySQL · PostgreSQL · SQLite


SEL is a small expression language for the rules applications live by — validation, pricing, eligibility, routing, reporting. A rule is text:

TOTAL = SUM(ITEMS, _["qty"] * _["price"]);
COND(IS_BLANK(CUSTOMER),                   ABORT("customer is required"),
     NOT RMATCH('^\d{2}-\d{3}$', POSTCODE), ABORT("postcode {POSTCODE} is not 00-000"),
     TOTAL > CREDIT_LIMIT,                  ABORT("total {TOTAL} exceeds {CREDIT_LIMIT}"),
     "ok")

Five independent implementations run it, held to one written specification, and they agree to the byte — on the value, and on the error code and position when a rule fails. The browser and the server, the batch job and the API, give one verdict.

Why SEL

  • Parity is the product. Exact decimal arithmetic — no floating point — no truthiness, strict UTF-8, a portable regex subset, and NULL that never turns into zero. Every host is held to a shared conformance suite, a differential fuzzer, a decimal oracle and documentation whose every example is executed. How parity is guaranteed →
  • Rules that reach the database. The same rule compiles to a SQL condition — knowing nothing about the schema, or with a description of it that makes the SQL tight and reaches related tables. A pipeline of FILTER, LINK, BUCKET, MAP and sorts compiles to a whole SELECT, and the hybrid planner pushes the longest exact prefix into the database and finishes the rest in memory — refusing, never guessing, wherever SQL would mean something else. SQL conditions → · SQL pipelines →
  • Easy to embed and to extend. Python, PHP and JavaScript have no dependencies; C++ is three files and a vendored regex engine; Lisp is an ASDF system. An application adds its own functions with one call, and its own SQL dialects and spellings the same way. Extending SEL →
  • Small on purpose. One expression per program. No loops, no user-defined functions, no dynamic names — so every rule terminates, and the inputs it reads are known before it runs. Overview →

A taste, in each language

Compile once, run per row:

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())
JavaScript
const rule = compile('IF(QTY * PRICE > LIMIT, "over budget", "ok")');
for (const row of [{ QTY: '3', PRICE: '19.99' }, { QTY: '1', PRICE: '5.00' }]) {
  const ctx = Value.fromNative({ ...row, LIMIT: '50.00' });
  console.log(`   QTY=${row.QTY} PRICE=${row.PRICE} =>`, rule.run(ctx).asText());
}
PHP
$rule = Sel::compile('IF(QTY * PRICE > LIMIT, "over budget", "ok")');
foreach ([['QTY' => '3', 'PRICE' => '19.99'], ['QTY' => '1', 'PRICE' => '5.00']] as $row) {
    $ctx = Value::fromNative($row + ['LIMIT' => '50.00']);
    printf("   QTY=%s PRICE=%s => %s\n", $row['QTY'], $row['PRICE'], $rule->run($ctx)->asText());
}
C++
const sel::Program rule = sel::compile("IF(QTY * PRICE > LIMIT, \"over budget\", \"ok\")");
for (const auto& row : std::vector<std::pair<std::string, std::string>>{
         {"3", "19.99"}, {"1", "5.00"}}) {
  sel::Value ctx = sel::Value::none();
  ctx.set("QTY", sel::Value::text(row.first));
  ctx.set("PRICE", sel::Value::text(row.second));
  ctx.set("LIMIT", sel::Value::text("50.00"));
  std::cout << "   QTY=" << row.first << " PRICE=" << row.second
            << " => " << rule.run(ctx).as_text() << "\n";
}
Common Lisp
(let ((rule (sel:compile-source "IF(QTY * PRICE > LIMIT, \"over budget\", \"ok\")")))
  (loop for (qty price) in '(("3" "19.99") ("1" "5.00"))
        do (format t "   QTY=~a PRICE=~a => ~a~%" qty price
                   (sel:as-text
                    (sel:run rule (ctx-of `(("QTY" . ,qty) ("PRICE" . ,price)
                                            ("LIMIT" . "50.00"))))))))

Each of these is part of examples/plain, and the five print byte-identical output — which the test suite checks, on the code shown here.

Documentation

Overview what SEL is, its principles, where it fits, what it leaves out
Parity how five hosts are made to agree, and how that is checked
Syntax · Operators · Functions the language
Using SEL the host API; a REPL, validation, scripting with host functions
SEL and SQL conditions; pipelines over star, EAV, 3NF, flat and complex data; reference
Extending · Contributing host functions, dialects, builtins; the order of work and the traps
Specification the normative text, with grammar and error codes

All of it is also a styled site with language tabs — see the documentation home to build or serve it, or to run it as a Docker image.

Install

The package is sel-lang everywhere:

pip install sel-lang
npm install sel-lang
composer require nathanjel/sel-lang
vcpkg install sel-lang            # or: conan install --requires sel-lang/0.8.1
(ql:quickload :sel-lang)          # Quicklisp / Ultralisp

Or copy python/sel/, js/src/ or php/src/ into a project — no package manager, no build step. Using SEL has the imports for each host, and PACKAGING.md the registries.

Checking it

tools/check.sh          # every layer, every host; prints ALL GREEN or it isn't done

Parity describes each layer, and Contributing the order of work.

Licence

MIT. Two third-party components keep their own BSD 2-Clause licences: SRELL, vendored into the C++ implementation, and cl-ppcre, which the Common Lisp system depends on; both are listed in LICENSE. The Python, PHP and JavaScript implementations have no dependencies at all.

Release files for sel-lang 0.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sel-lang 0.8.1
File Size Uploaded
sel_lang-0.8.1.tar.gz 505.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sel-lang 0.8.1
File Interpreter ABI Platform
sel_lang-0.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 695.0 kB

Release files / sel_lang-0.8.1.tar.gz

Download URL sel_lang-0.8.1.tar.gz
Size 505.1 kB
Tags Source
SHA-256 checksum
How to use checksums
aff35853b12cd1d72ecbc3b123436d4dcedaffd157fa5c1f8567ab82fa7138d1
BLAKE2b-256 checksum
How to use checksums
7645fcc2efc732e473dcb8927748b328fec80de19844d454e86c091688ef1f09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / sel_lang-0.8.1-py3-none-any.whl

Download URL sel_lang-0.8.1-py3-none-any.whl
Size 189.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dbde95ffbb0add63dbebd2c8c20ce945060094914a50b27ed58f98db9b8fe352
BLAKE2b-256 checksum
How to use checksums
6477fbbe6f7a64df0c0e2e1f3a05e46fe14455125311dc27a2715dd4da940ea3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

0.8.1 This release

2 release files

0.8.0

2 release files

0.7.4

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page