HEL — Heuristics Expression Language
Status: OPEN — Apache-2.0
SPDX-License-Identifier: Apache-2.0
Overview
- HEL (Internally Hermes Expression Language) is a small, deterministic, auditable expression language and reference implementation.
- This crate implements the open core: a pest-based parser, a compact typed AST, deterministic evaluator(s), a pluggable builtins registry, schema/package loaders for domain types, and a trace facility that produces stable, auditable evaluation traces.
- The crate is intentionally product-agnostic: domain-specific or proprietary built-ins and rule packs should be implemented and shipped separately and injected at runtime via the builtins provider interface.
Goals
- Determinism: evaluation order and iteration are stable (stable maps, deterministic traces).
- Auditability: fine-grained atom-level traces that show resolved inputs and atom results.
- Extensibility: runtime injection of domain built-ins via a clear provider/registry API.
- Minimal surface area: provide primitives (parser, AST, evaluator, trace, schema loader) rather than a monolithic runtime.
What this crate provides (public capabilities)
- Parsing
- A pest grammar (
hel.pest) and parser typeHelParser. - Top-level parse helper:
parse_rule(condition: &str) -> AstNode.
- A pest grammar (
- AST
AstNodevariants:Bool,String,Number,Float,Identifier,Attribute,Comparison,And,Or,ListLiteral,MapLiteral,FunctionCall.- Comparators supported:
==,!=,>,>=,<,<=,CONTAINS,IN.
- Evaluation
- Resolver-based evaluation:
evaluate_with_resolver(condition: &str, resolver: &dyn HelResolver) -> Result<bool, EvalError>. - Evaluation with builtins:
evaluate_with_context(condition: &str, resolver: &dyn HelResolver, builtins: &builtins::BuiltinsRegistry). - Runtime
Valuemodel:Null,Bool,String(Arc<str>),Number(f64),List(Vec<Value>),Map(BTreeMap<Arc<str>, Value>). HelResolvertrait for embedding hosts to supply attribute values (object.field).
- Resolver-based evaluation:
- Builtins and extensibility
BuiltinsProvidertrait andBuiltinsRegistryfor namespace-aware function dispatch.BuiltinFntype: pure, deterministic functions that map argumentValues to aResult<Value, EvalError>.CoreBuiltinsProviderincluded with a small set of generic functions (core.len,core.contains,core.upper,core.lower).
- Trace & audit
evaluate_with_trace(condition, resolver, Option<&BuiltinsRegistry>) -> Result<EvalTrace, EvalError>.EvalTracecontains a deterministic list ofAtomTraceentries and a sorted list offacts_used().- Pretty-print helpers to produce deterministic, human-readable traces.
- Schema and package system
- Schema parser and in-memory
Schemarepresentation (FieldType,TypeDef,FieldDef). - Package manifest type
PackageManifest(hel-package.toml),SchemaPackage, andPackageRegistryfor loading and resolving packages into aTypeEnvironment. - Deterministic package resolution and type merging with clear collision detection.
- Schema parser and in-memory
Quick usage examples
- Parse an expression into an AST:
use hel::parse_rule;
let ast = parse_rule("binary.format == \"elf\" AND security.nx_enabled == true");
// `ast` is an `AstNode` representing the parsed expression
- Evaluate with a simple resolver:
use hel::{evaluate_with_resolver, HelResolver, Value};
struct MyResolver;
impl HelResolver for MyResolver {
fn resolve_attr(&self, object: &str, field: &str) -> Option<Value> {
match (object, field) {
("binary", "format") => Some(Value::String("elf".into())),
("security", "nx_enabled") => Some(Value::Bool(true)),
_ => None,
}
}
}
let resolver = MyResolver;
let result = evaluate_with_resolver(r#"binary.format == "elf""#, &resolver)?;
assert!(result);
- Evaluate with builtins and capture a trace:
use hel::{evaluate_with_trace, HelResolver, builtins::BuiltinsRegistry, builtins::CoreBuiltinsProvider};
let mut registry = BuiltinsRegistry::new();
registry.register(&CoreBuiltinsProvider)?;
struct MyResolver;
impl HelResolver for MyResolver {
fn resolve_attr(&self, object: &str, field: &str) -> Option<hel::Value> { /* ... */ unimplemented!() }
}
let trace = evaluate_with_trace("core.len([1,2,3]) == 3", &MyResolver, Some(®istry))?;
println!("{}", trace.pretty_print()); // deterministic, human-friendly audit trail
Design notes and important details
- Determinism
- Internal maps use
BTreeMapand lists are iterated stably to ensure deterministic behavior across runs. - Traces and
facts_used()are sorted to make audit logs stable.
- Internal maps use
- Pure builtins
- Builtins must be pure and deterministic; they must not perform unbounded I/O or rely on global mutable state. The registry enforces namespace isolation and stable ordering.
- Error handling
- Public evaluation functions return
Result<..., EvalError>.EvalErrorcovers parse errors, type mismatches, unknown attributes, and invalid operations.
- Public evaluation functions return
- Limits & omissions
- The core language focuses on declarative expressions and comparisons. It does not provide arithmetic operators (
+,-,*,/) beyond numeric comparisons in the current implementation. - Function calls require a
BuiltinsRegistryin the evaluation context. Without it, invokingFunctionCallyields anInvalidOperationerror. - The crate exposes primitives (parser, AST, evaluator, trace, schema loader) and intentionally does not provide a single monolithic "compiler" or product-specific rule engine.
- The core language focuses on declarative expressions and comparisons. It does not provide arithmetic operators (
- Performance & safety
- The evaluator uses
f64for runtime numbers; integer literal parsing persistsu64in the AST then converts as needed toValue::Number(f64). - Avoid unbounded regexes in any custom builtins. The crate itself does not depend on a regex engine; pattern-match builtins must ensure bounded, deterministic execution.
- The evaluator uses
Documentation and where to look next
- Read the
srcmodules to get API-level details:hel::schema— package manifest,SchemaPackage, schema parsing helpers.hel::builtins— provider/registry API andCoreBuiltinsProvider.hel::trace— trace capture and pretty-print helpers.hel::parse_ruleand the AST insrc/lib.rs.
- Local docs:
docs/USAGE.mdanddocs/SCHEMA.md(examples and schema/package format). - Tests in
src/*demonstrate intended semantics and edge-case behavior (NaN handling, builtins, trace order, package registry collision detection).
Contributing
- Follow these principles when contributing:
- Preserve determinism and auditability.
- Keep open built-ins generic and product-agnostic.
- When adding features that affect evaluation semantics, add deterministic tests and trace-based examples.
- Avoid exposing
unsafein public APIs unless strictly necessary and justified with clear documentation.
License
- Apache-2.0. Open builtins included here must follow the same license. Product-specific or proprietary builtins and rule packs belong in separate crates and should be injected through
BuiltinsProvider.