Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

blockwatcher-expr

blockwatcher-expr is the internals of blockwatcher’s default predicate language and the expr matcher module that wraps it: a lexer, a recursive-descent parser, a write-time type-checker, and a total three-valued evaluator, plus the ExprMatcher port implementation and module registration that make the whole pipeline selectable as matcher = "expr" in blockwatcher.toml. It is a module crate: chain-agnostic itself, and the one Matcher in the shipped module catalog (see Modules), though the port boundary in blockwatcher-ports never limits a deployment to it.

This page covers the crate’s own internals: parsing, type-checking, compilation, and evaluation. It does not restate the predicate language’s grammar, operator table, or type system, which Predicates and the expression language already documents in full from an operator’s point of view; where the two overlap, this page links there rather than duplicating it.

Key takeaways

  • blockwatcher-expr is the predicate language’s internals: a lexer, a recursive-descent parser, a write-time type-checker, and a total three-valued evaluator, plus the ExprMatcher port implementation.
  • It is chain-agnostic itself and the one Matcher in the shipped module catalog, though the port boundary never limits a deployment to it.
  • This page covers the crate’s own parsing, type-checking, compilation, and evaluation internals; the language’s grammar and type system are already documented from an operator’s point of view on Predicates.

Responsibilities

  • Scan predicate source text into an ordered stream of tokens, each one tagged with the byte range it came from, decoding numeric, hex, and string literals as it goes so nothing downstream re-parses them (lexer.rs).
  • Parse tokens into a spanned Ast via recursive descent, one function per precedence level, enforcing three bounds (nesting depth, tree height, and source length) so a hostile predicate cannot exhaust the stack or the heap at compile time (parser.rs).
  • Type-check the Ast against a monitor’s SchemaSet, admitting or rejecting every operator’s operands and producing an operator-facing PredicateError (with an edit-distance did-you-mean suggestion for an unknown field or namespace) the moment a predicate is written (typecheck/mod.rs, typecheck/diagnostics.rs).
  • Evaluate a type-checked Ast against a DecodedEvent with three-valued (Kleene) logic: total, allocation-light, and never a panic (eval.rs).
  • Wrap the whole pipeline as Predicate (compile/matches/explain/ referenced_fields) and as ExprMatcher, the blockwatcher-ports::Matcher implementation the expr module registers under (lib.rs, matcher.rs).
  • (testing feature) Expose test_schemas::schemas() (the fixture this crate’s own tests, its fuzz targets, and its checked-in fuzz corpus all compile predicates against) to external consumers, most notably the crate’s own tests/corpus_replay.rs, which links blockwatcher-expr as a dev-dependency with testing enabled specifically to reach it.

Not this crate’s job: defining the Matcher trait, SchemaSet, or ValueType it type-checks against (blockwatcher-ports, blockwatcher-types); choosing which matcher module a deployment runs (blockwatcher.toml, read by blockwatcher-core); shipping a second predicate language: none exists, and nothing about the port boundary requires this one.

Key types and traits

NameKindRole
PredicatestructA predicate that already passed parsing and type-checking; compile is its sole constructor, which is what lets matches run without any failure path
ExprMatcherstructThe blockwatcher-ports::Matcher implementation wrapping Predicate
Registry, ExprConfigstructThe ModuleRegistry registration for "expr"; ExprConfig is an empty, deny_unknown_fields struct, since the module takes no configuration of its own
Ast, Nodestruct/enumThe parsed tree: a Node paired with the byte Span it was parsed from
BinOpenumThe 17 binary operators the grammar admits
FamilyenumThe type-checker’s own, coarser type vocabulary (Int/Str/Bytes/Bool/Array/Map): Int collapses ValueType::Int/Uint, Bytes collapses Address/Bytes
NodeTypestructA node’s computed type: its Family set, whether every integer declaration behind a path is Uint-only, and a literal’s constant value (the state families/bin_type thread through the type-check walk)
TruthenumThe evaluator’s three-valued result: True / False / Unknown

How data flows through it

flowchart LR
    S["source text"] -->|"lexer::lex"| T["tokens"]
    T -->|"parser::parse"| A["Ast<br/>(untyped)"]
    A -->|"typecheck::check(schemas)"| P["Predicate<br/>(typed AST, compiled)"]
    P -->|"eval::evaluate(event)"| Tr["Truth"]
    Tr -->|"== Truth::True"| M["matches(): bool"]
    A -.->|"lex/parse/type error"| E["PredicateError"]

typecheck::check does not transform the tree: it walks the same Ast parser::parse returned and either accepts it or fails with a PredicateError; the “typed AST” in the diagram above is that same tree plus the guarantee that every path resolved and every operator’s operands admitted it. Predicate::compile (lib.rs) runs the whole left half of this diagram in order (parse, then check), and only a result that survived both becomes a Predicate; matches (lib.rs) is the whole right half, eval::evaluate(&self.ast, event) == eval::Truth::True, over the canonical Value tree a DecodedEvent carries. explain and referenced_fields are separate walks over the same typed Ast: the former re-evaluates and renders the failing subtree, the latter just collects every Node::Path. Neither one is on this diagram’s critical path, since both run only when a caller asks for them.

Neighbours

blockwatcher-expr depends on the following in production:

  • blockwatcher-types: vocabulary and schemas
  • blockwatcher-ports: trait boundary for matcher port
  • serde: (de)serialization derive
  • serde_json: JSON wire format
  • num-bigint: arbitrary-precision integers

In [dev-dependencies]:

  • blockwatcher-expr (at path = "." with testing feature): allows tests/corpus_replay.rs to reach test_schemas::schemas() under bare cargo test
  • blockwatcher-ports (fakes feature): FakeMatcher for cross-checking in matcher.rs tests
  • indexmap: ordered maps
  • proptest: property testing
  • tokio: async runtime

Only the following crate depends on it directly (per the dependency table):

  • blockwatcher-embed: the composition façade registers the matcher into the engine’s module catalog; the blockwatcher binary reaches this crate only through embed

Reading the source

  1. Start at lib.rs: the module list, Predicate’s public surface, and the doc comment on the compile/evaluate field-addressing contract every decoder must honor (pinned end to end by tests/dotted_field_addressing.rs).
  2. lexer.rs: Token, Span, and lex; token-decimal expansion and the two string escapes it recognizes.
  3. parser.rs: Ast, Node, BinOp, the recursive-descent grammar, and the three compile-time bounds (MAX_DEPTH, MAX_TREE_HEIGHT, MAX_SOURCE_LEN), each checked right where a hostile input could exceed it rather than by a separate validation pass afterward.
  4. typecheck/mod.rs: Family, NodeType, families/bin_type (the per-operator-class admission rules), and check, the entry point Predicate::compile calls.
  5. typecheck/diagnostics.rs: every rejection message’s exact wording and the bounded edit-distance search behind the did-you-mean suggestion.
  6. typecheck/message_guard.rs (cfg(test) only): a falsification harness that reads every rejection message as a claim and checks it against every predicate the checker accepts, so a message cannot assert something an accepted predicate disproves.
  7. eval.rs: Truth, the three-valued Kleene evaluate, and explain/explain_failed, the dry-run diagnostic walk.
  8. matcher.rs: ExprMatcher (the Matcher port implementation), Registry/ExprConfig (the "expr" module registration), and matchers::get_all().
  9. test_schemas.rs: the schema fixture behind the testing feature, shared by this crate’s own tests, its proptest suites, its fuzz targets, and tests/corpus_replay.rs.