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
NULLthat 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,MAPand sorts compiles to a wholeSELECT, 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)
| File | Size | Uploaded | |
|---|---|---|---|
| sel_lang-0.8.1.tar.gz | 505.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|