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:
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())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());
}$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());
}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";
}(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 / UltralispOr 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 doneParity 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.