nathanjel / sel-lang
A small expression language for validation rules — one rule file evaluates identically on PHP, JavaScript, Python, C++23 and Common Lisp. Exact decimal arithmetic, no floating point, no truthiness.
Requires
- php: >=8.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-04 18:45:55 UTC
README
Write a business rule once. Run it in seven languages — or in your database — and get the same answer.
Python · JavaScript · PHP · C++23 · Common Lisp · Rust · Go | 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")
Seven 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, JavaScript and Go have no dependencies; C++ is three files and a vendored regex engine; Lisp is an ASDF system; Rust is a crate on the regex crate (and serde_json for the SQL layer). 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"; }
Rust
let mut rule = compile("IF(QTY * PRICE > LIMIT, \"over budget\", \"ok\")")?; for (qty, price) in [("3", "19.99"), ("1", "5.00")] { let ctx = Value::none(); ctx.set("QTY", val(qty), at)?; ctx.set("PRICE", val(price), at)?; ctx.set("LIMIT", val("50.00"), at)?; println!(" QTY={qty} PRICE={price} => {}", text(&rule.run(Some(ctx))?)?); }
Go
rule, err := sel.Compile(`IF(QTY * PRICE > LIMIT, "over budget", "ok")`) check(err) for _, row := range [][2]string{{"3", "19.99"}, {"1", "5.00"}} { ctx := sel.NewNone() ctx.Set("QTY", sel.NewText(row[0])) ctx.Set("PRICE", sel.NewText(row[1])) ctx.Set("LIMIT", sel.NewText("50.00")) v, err := rule.Run(ctx) check(err) fmt.Printf(" QTY=%s PRICE=%s => %s\n", row[0], row[1], v.AsText(at)) }
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 seven print
byte-identical output — which the test suite checks, on the code shown here.
The Rust and Go tabs use a few small helpers from that file: at is "the host"
as a position (Pos::default(), sel.Pos{}); Rust's val(s) is
Value::text_owned(s.to_string()) and text(v) is v.as_text(at); Go's
check(err) stops on an error a well-formed program never has, and
eval(src) is sel.Eval(src, nil) with that check.
Documentation
| Overview | what SEL is, its principles, where it fits, what it leaves out |
| Parity | how seven 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.10.0 (ql:quickload :sel-lang) # Quicklisp / Ultralisp cargo add sel-lang # the crate is sel_lang go get github.com/nathanjel/sel/go@v0.10.0 # import .../go/sel and .../go/sel/sql
The command-line REPL (sel -e 'expr', sel --deps -e 'expr') installs with
cargo install sel-lang or go install github.com/nathanjel/sel/go/bin/sel@v0.10.0.
In a browser, with nothing to install, from the jsDelivr (or unpkg) copy of the npm package — a standalone module, without the SQL layer:
<script type="module"> import { evaluate } from 'https://cdn.jsdelivr.net/npm/sel-lang@0.10.0/dist/sel.min.mjs'; </script>
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, JavaScript and Go implementations have no dependencies at all; the Rust crate depends on the regex crates, and, for its SQL layer, on serde and serde_json.