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.
Quick Start
HEL provides a simple, high-level API for expression validation and evaluation:
Basic Expression Validation
use validate_expression;
// Validate syntax without evaluation
let expr = r#"binary.arch == "x86_64" AND security.nx == false"#;
validate_expression?; // Returns Ok(()) or detailed parse error
Expression Evaluation with Facts
use ;
// Create evaluation context with facts
let mut ctx = new;
ctx.add_fact;
ctx.add_fact;
// Evaluate expression
let expr = r#"binary.arch == "x86_64" AND security.nx == false"#;
let result = evaluate?; // Returns true
Script Files with Let Bindings
HEL supports .hel script files with reusable let bindings:
use ;
let mut ctx = new;
ctx.add_fact;
ctx.add_fact;
let script = r#"
# Define reusable sub-expressions
let has_sms_perms =
manifest.permissions CONTAINS "READ_SMS" AND
manifest.permissions CONTAINS "SEND_SMS"
let has_obfuscation = binary.entropy > 7.5
# Final boolean expression
has_sms_perms AND has_obfuscation
"#;
let result = evaluate_script?; // Returns true
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)
Expression Validation and Parsing
- Expression Validation:
validate_expression(expr: &str) -> Result<(), HelError>- validate syntax without evaluation - Expression Parsing:
parse_expression(expr: &str) -> Result<Expression, HelError>- parse into AST - Script Parsing:
parse_script(script: &str) -> Result<Script, HelError>- parse.helfiles with let bindings
Expression Evaluation
- Simple Evaluation:
evaluate(expr: &str, context: &FactsEvalContext) -> Result<bool, HelError>- evaluate with facts - Script Evaluation:
evaluate_script(script: &str, context: &FactsEvalContext) -> Result<bool, HelError>- evaluate scripts with let bindings - Advanced Evaluation: Resolver-based evaluation via
evaluate_with_resolver()andevaluate_with_context()
Context and Data
- FactsEvalContext: Simple key-value store for facts (e.g., "binary.arch" -> "x86_64")
- HelResolver trait: Custom attribute resolution for advanced integrations
- Value type:
Null,Bool,String,Number,List,Map
Error Handling
- HelError: Enhanced error type with line/column information for parse errors
- EvalError: Evaluation-time errors (type mismatches, unknown attributes, etc.)
- Clear error messages for common mistakes
Legacy APIs
- Low-level Parsing:
parse_rule(condition: &str) -> AstNode- direct AST construction - AST:
AstNodevariants:Bool,String,Number,Float,Identifier,Attribute,Comparison,And,Or,ListLiteral,MapLiteral,FunctionCall - Comparators:
==,!=,>,>=,<,<=,CONTAINS,IN
Builtins and Extensibility
BuiltinsProvidertrait andBuiltinsRegistryfor namespace-aware function dispatchBuiltinFntype: pure, deterministic functions that map argumentValues to aResult<Value, EvalError>CoreBuiltinsProviderincluded with generic functions (core.len,core.contains,core.upper,core.lower)
Trace & Audit
evaluate_with_trace(condition, resolver, Option<&BuiltinsRegistry>) -> Result<EvalTrace, EvalError>EvalTracecontains deterministic list ofAtomTraceentries and sorted list offacts_used()- Pretty-print helpers for 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, andPackageRegistry - Deterministic package resolution and type merging with collision detection
Integration with Rule Engines
HEL is designed to be embedded in rule engines and security analysis tools. Here's how to integrate HEL into your application:
Example: Malware Detection Rule Engine
use ;
use fs;
Example Rule File: android-malware.hel
# Check for suspicious SMS permissions
let has_sms_perms =
manifest.permissions CONTAINS "READ_SMS" AND
manifest.permissions CONTAINS "SEND_SMS"
# Check for code obfuscation indicators
let has_obfuscation =
binary.entropy > 7.5 OR
strings.count < 10
# Final detection logic
has_sms_perms AND has_obfuscation
Best Practices for Integration
-
Validation Before Deployment: Always validate rule scripts before loading them:
let script = read_to_string?; validate_expression?; // Catch syntax errors early -
Error Handling: Distinguish between parse errors (rule bugs) and evaluation errors (data issues):
match evaluate_script -
Performance: Parse scripts once and reuse the AST:
let parsed = parse_script?; // Store parsed.bindings and parsed.final_expr // Reuse for multiple evaluations
Advanced 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.