hel 0.1.0

HEL — Hermes Expression Language: a deterministic, auditable expression language & parser, AST, builtin registry and evaluator.
Documentation

HEL — Hermes Expression Language (Rust crate)

Status: OPEN — Apache-2.0
SPDX-License-Identifier: Apache-2.0

Overview

  • HEL (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 type HelParser.
    • Top-level parse helper: parse_rule(condition: &str) -> AstNode.
  • AST
    • AstNode variants: 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 Value model: Null, Bool, String(Arc<str>), Number(f64), List(Vec<Value>), Map(BTreeMap<Arc<str>, Value>).
    • HelResolver trait for embedding hosts to supply attribute values (object.field).
  • Builtins and extensibility
    • BuiltinsProvider trait and BuiltinsRegistry for namespace-aware function dispatch.
    • BuiltinFn type: pure, deterministic functions that map argument Values to a Result<Value, EvalError>.
    • CoreBuiltinsProvider included 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>.
    • EvalTrace contains a deterministic list of AtomTrace entries and a sorted list of facts_used().
    • Pretty-print helpers to produce deterministic, human-readable traces.
  • Schema and package system
    • Schema parser and in-memory Schema representation (FieldType, TypeDef, FieldDef).
    • Package manifest type PackageManifest (hel-package.toml), SchemaPackage, and PackageRegistry for loading and resolving packages into a TypeEnvironment.
    • Deterministic package resolution and type merging with clear collision detection.

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(&registry))?;
println!("{}", trace.pretty_print()); // deterministic, human-friendly audit trail

Design notes and important details

  • Determinism
    • Internal maps use BTreeMap and lists are iterated stably to ensure deterministic behavior across runs.
    • Traces and facts_used() are sorted to make audit logs stable.
  • 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>. EvalError covers parse errors, type mismatches, unknown attributes, and invalid operations.
  • 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 BuiltinsRegistry in the evaluation context. Without it, invoking FunctionCall yields an InvalidOperation error.
    • The crate exposes primitives (parser, AST, evaluator, trace, schema loader) and intentionally does not provide a single monolithic "compiler" or product-specific rule engine.
  • Performance & safety
    • The evaluator uses f64 for runtime numbers; integer literal parsing persists u64 in the AST then converts as needed to Value::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.

Documentation and where to look next

  • Read the src modules to get API-level details:
    • hel::schema — package manifest, SchemaPackage, schema parsing helpers.
    • hel::builtins — provider/registry API and CoreBuiltinsProvider.
    • hel::trace — trace capture and pretty-print helpers.
    • hel::parse_rule and the AST in src/lib.rs.
  • Local docs: docs/USAGE.md and docs/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 unsafe in 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.