erdl-formal
🚀 POC welcome — we encourage you to try this project as a proof of concept in your own environment. For technical support, contact us anytime at support@openoba.com.
A formal verifier for the ERDL expression kernel — proving rule safety over all inputs with Z3.
There are two kinds of determinism: the determinism tests cover, and the determinism mathematics proves. erdl-formal provides the latter.
"erdl-formal formally verifies ERDL v2.1's expression kernel and evaluation semantics (§5, §7, Appendix A), covering all 34 nodes and the evaluation-semantics constraints E2/E8/E10/E11/E12 (plus E3 division-by-zero). The engine-layer constraints E1 (purity), E4 (resource limits), E5 (when/expr exclusivity), E6 (tree hashing), E7 (single evaluator), and E9 (no wall clock) are runtime/compile-layer concerns, not kernel evaluation semantics — the spec's other layers (document structure, gloss rendering, integration modes) are guaranteed by test vectors and engineering verification."
Why now: LLMs have brute force. They don't have direction.
LLMs are probabilistic: same input, different answers. Hand enterprise decisions to a probability distribution, and the auditors will eventually ask — "on what basis was this decision made?"
The industry is converging on an answer: let the LLM understand; let a deterministic rule engine decide. But a rule engine's "determinism" is usually underwritten by unit tests — and tests only prove the inputs you happened to write.
In regulated industries — finance, insurance, government — the audit question is singular:
Does this rule hold for every input?
Sampled tests can't answer that. Only a proof can. Cedar Analysis proved this approach works inside AWS (Lean formalization + SMT symbolic analysis). erdl-formal does the same for the ERDL expression kernel — lifting determinism from sampled testing to exhaustive proof.
- Cedar Analysis: used inside AWS, Lean + SMT, not open to the public, serving only AWS's own policy language.
- OPA/Rego: no formal semantics — "the implementation is the spec."
- erdl-formal: open source, 34-node full coverage, ERDL-specific semantics (money, time, decision objects, bidirectional gloss).
One proof in 30 seconds
The G3 classification gate: file classification > operator classification → DENY. We want to prove the rule is both reachable and fail-closed (a missing field never opens a bypass):
from erdl_formal.field_contracts import FieldContract, Schema
from erdl_formal.properties import always_denies
schema = Schema()
schema.add(FieldContract(field="file_cls", type="int"))
schema.add(FieldContract(field="op_cls", type="int"))
# when: file_cls > op_cls → DENY
rule = ["gt", ["field", "file_cls"], ["field", "op_cls"]]
# One assertion, two properties at once:
# 1) reachable — with both fields present, a higher classification fires the block
# 2) fail-closed — a missing field never opens a bypass. This depends on the
# document's unmatched fallback (default_decision): under a DENY fallback the
# silenced guard still denies; under the ALLOW fallback (resolution default)
# the missing field falls through to ALLOW — the rule fails open.
assert always_denies(
rule, schema,
premises=["file_cls", "op_cls"],
missing_field="op_cls",
default_decision="DENY",
)
assert not always_denies(
rule, schema,
premises=["file_cls", "op_cls"],
missing_field="op_cls",
default_decision="ALLOW",
)
No test cases. No sampling. Z3 searches the space of all integers for an input violating the property: if one exists, you get a concrete, replayable counterexample (re-checkable against the real engine); if not, UNSAT — the property holds for every input. QED.
What you can prove
| Property | Meaning | Origin |
|---|---|---|
| always-denies | fires whenever its guard can — whether a missing field opens a bypass is resolved against default_decision |
Cedar + E11 |
| subsumption / equivalence / disjointness | implication / equivalence / mutual exclusion | Cedar |
| override-soundness | overrides go DENY→ALLOW only (never toward a less-safe state) | ERDL-specific |
| ring-respect | a higher-ring DENY overrides a lower-ring ALLOW (ring order honored in the DENY direction) | ERDL-specific |
| emergency-shortcut | EMERGENCY_HALT short-circuits the moment it fires | ERDL-specific |
| workflow-shortcut | WORKFLOW short-circuits into its state machine the moment it fires | ERDL-specific |
| catch-all-inert-when-explicit | a catch-all (empty-condition) rule never takes effect when an explicit-condition rule is present | ERDL-specific |
| catch-all-then-irrelevant | a catch-all's then value is irrelevant to the final decision when an explicit rule is present |
ERDL-specific |
| catch-all-override-irrelevant | a catch-all's override value is irrelevant to the final decision when an explicit rule is present |
ERDL-specific |
Expression-layer properties (always_denies / subsumes / …) are proven by writing the negation as constraints and checking UNSAT; the ERDL-specific resolution properties (override_soundness / ring_respect / emergency_shortcut / workflow_shortcut / catch_all_inert_when_explicit / catch_all_then_irrelevant_when_explicit / catch_all_override_irrelevant_when_explicit) are encoded in resolution_smt.py, which compiles resolve()'s ring / override / priority ordering into Z3 constraints and proves them UNSAT over every rule-set of exactly n modeled positions (a bounded exhaustive proof at cardinalities n∈{2,3,4}, not sampling — but also not an unbounded proof over arbitrary length). Any SAT counterexample is a concrete rule-set replayed against the real engine — proof plus differential testing, double insurance.
34/34 nodes, E2/E8/E10/E11/E12 evaluation semantics
- All 34 nodes have SMT encodings: value / logic / comparison / set / string / existence / quantifier / arithmetic / time / aggregate (comparison and existence dispatch by field type: int / string / bool;
epoch_mssupports date-only and ISO 8601 datetime with time / offset). - The hard semantics aren't "roughly right" — they are bit-exact:
- E2 fixed-point decimals: scale=14 + half-even — money is not allowed
0.1 + 0.2drift; - E8 quantifier empty-array folding: anti-vacuous-truth —
all([])is false, not true; - E11 three-valued logic: missing fields collapse to false at the leaves (not Kleene); whether a missing field fails open is resolved by the document fallback (see
always_denies'sdefault_decision); - E10 NFC normalization; E12 evaluation errors collapse to
Missing(tier≤2 fail-close is runtime behavior, not modeled in the kernel).
- E2 fixed-point decimals: scale=14 + half-even — money is not allowed
Independent verifier: three-way, byte-for-byte
A verifier is only credible if it never peeks at the examinee's answers. erdl-formal encodes the spec alone (erdl-language-spec), with zero dependency on any ERDL engine implementation, forming a three-way independent cross-check with erdl (the TS engine) and erdl-vectors (frozen vectors):
| Cross-check | Pair | Result |
|---|---|---|
| Fixed-point | fixed_point.py ↔ erdl fixed-point.js |
byte-identical |
| Resolution | resolution.py ↔ resolution_smt.py (Z3 model; semantics aligned to erdl-landing evaluator.ts) |
exhaustive + random differential (test_resolution_smt.py) |
| Arithmetic vectors | ↔ erdl-vectors V-ENGINE | 7/7 |
| Calendar vectors | ↔ erdl-vectors V-ENGINE | 6/6 |
| G3 counterexample replay | tvl.py ↔ erdl engine |
scenario-identical |
Measurements, not endorsements. Three independently built systems agreeing byte-for-byte means the spec is precise enough to sustain exact independent reimplementation.
How it differs from Cedar / OPA
| Dimension | Cedar Analysis | OPA / Rego | erdl-formal |
|---|---|---|---|
| Formal verification | ✅ Lean + SMT (the field's benchmark, closed) | ❌ no formal semantics — the implementation is the spec | ✅ SMT (Z3), open source |
| Fixed-point (money) | decimals only via extension plugin | float64 loses precision | ✅ scale=14 + half-even |
| Time / calendar | — | — | ✅ UTC calendar (days_between / date_add / date_part / month-end) |
| Aggregation | — | — | ✅ aggregate (count / sum / avg / min / max) |
| Quantifiers | — | — | ✅ all / any / none (E8 empty-array fold) |
| Decision object | policy IDs only | unsigned logs | ✅ rich DO + hash chain |
| Natural language | one-way (NL→policy) | one-way | ✅ deterministic gloss, anchored round-trip (two-way) |
The difference is not "more formal" — Cedar's stack is the benchmark, and we say so. The difference is what gets formalized: ERDL is an enterprise rule kernel built for money, time, aggregation, quantifiers, decision objects, and two-way natural language — none of which exist in the Cedar / OPA world (the — rows above).
Architecture
ERDL rule (S-expression)
│
▼ compiler.py (symbolic compiler: schema-driven field typing + quantifier index unfolding)
Z3 TVL expressions (TVL(τ) = Def | Missing, leaf collapse)
│
▼ properties.py (property checks) / resolution.py (resolution reference model)
sat / unsat + counterexample (replayed against the real engine)
Installation
Users (from PyPI, one command):
pip install erdl-formal
Developers (from source, editable install, changes take effect immediately):
git clone https://github.com/OpenOBA/erdl-formal.git
cd erdl-formal
python -m pip install -e ".[dev]" # Python ≥3.11 (developed on 3.14), z3-solver ≥4.13 (verified on 5.1.0)
Build a distribution (wheel + sdist, for release / offline distribution):
python -m pip install build
python -m build # produces dist/erdl_formal-<version>-py3-none-any.whl + .tar.gz
Quick start
pytest # all passing
python examples/verify_g3.py # prove the G3 classification rule (reachable + fail-closed)
python replay/crosscheck-vectors.py # cross-check against erdl-vectors frozen vectors
Documentation
docs/semantics.en.md— denotational semantics for all 34 nodesdocs/tvl-encoding.en.md— three-valued logic SMT encodingdocs/field-contracts.en.md— verification schema contractsdocs/DEVELOPER-GUIDE.en.md— developer guide (architecture / adding nodes / properties / API / build & publish)
Acknowledgments
The resolution-layer properties and their assurance boundary were sharpened by
external review, recorded below as reviewer → contribution → effect (all
reproducible under replay/):
ANP2 Network (dev.to/anp2network) — two rounds of review of the resolution semantics, three findings (each closed as fix + test + proof):
- string/bool
eq/ne/exists(v0.1.2) — the kernel only compiled int equality/existence, raisingSort mismatchon string equality. always_deniesfail-closure direction (v0.1.2) — the property proved silence rather than fail-closure, backwards under an ALLOW fallback.- catch-all relax-direction gap (v0.1.17) — a catch-all (empty-condition) ALLOW could rewrite an explicit DENY across rings, with no property watching the relax direction.
RavindraAnnam (github.com/RavindraAnnam)
— reviewed resolution_smt.py / test_resolution_smt.py against SPEC §7.1:
- bounded-exhaustive wording — the properties are proven UNSAT at each checked cardinality (n ∈ {2,3,4}), not over arbitrary rule-set length; the wording now states exactly that (a bounded exhaustive proof, not an unbounded claim).
- witness-bound induction obligation — named the induction that would turn
the "≤2-rule witness" claim into a small-model theorem; now carried by
support_lemma(deletion invariance at any sorted position, UNSAT at n ∈ {4,5}) +minimality(≤2 to n=6, all seven properties) + structural analysis (each bad condition involves ≤2 rules), with the open last mile — Z3-formalized structural analysis and unbounded induction (n ≥ 7) — honestly marked.
Contributing & security
CONTRIBUTING.md— contribution process (issue-first, review)SECURITY.md— report vulnerabilities privately, no public issueCHANGELOG.md— changelog (Keep a Changelog)CODE_OF_CONDUCT.md/style_guide.md
License
Apache-2.0 · © 2026 深圳市秒镜科技有限公司 (Shenzhen Miaojing Technology Co., Ltd.)
Metadata
Release files for erdl-formal 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| erdl_formal-0.2.0.tar.gz | 73.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| erdl_formal-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 122.9 kB
Release files / erdl_formal-0.2.0.tar.gz
| Download URL | erdl_formal-0.2.0.tar.gz |
|---|---|
| Size | 73.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6356739bb3a912112a637ba0705931922d967a793c3e6d48401c4c6b6a5e4307
|
|
BLAKE2b-256 checksum How to use checksums |
a6752c812c311af1743f3c4cc4780e2c31a36d179741e7e699d1e175e87e85c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / erdl_formal-0.2.0-py3-none-any.whl
| Download URL | erdl_formal-0.2.0-py3-none-any.whl |
|---|---|
| Size | 49.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33cb8871e73b3bcd2506467de3f2527a1a9a1774111e4d84a50c7c14258f3ffa
|
|
BLAKE2b-256 checksum How to use checksums |
183a0251f39c333e7832002fafd60c9da5b25608084323407147bf9be5d77340
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|