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-expris the predicate language’s internals: a lexer, a recursive-descent parser, a write-time type-checker, and a total three-valued evaluator, plus theExprMatcherport implementation.- It is chain-agnostic itself and the one
Matcherin 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
Astvia 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
Astagainst a monitor’sSchemaSet, admitting or rejecting every operator’s operands and producing an operator-facingPredicateError(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
Astagainst aDecodedEventwith 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 asExprMatcher, theblockwatcher-ports::Matcherimplementation theexprmodule registers under (lib.rs,matcher.rs). - (
testingfeature) Exposetest_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 owntests/corpus_replay.rs, which linksblockwatcher-expras a dev-dependency withtestingenabled 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
| Name | Kind | Role |
|---|---|---|
Predicate | struct | A predicate that already passed parsing and type-checking; compile is its sole constructor, which is what lets matches run without any failure path |
ExprMatcher | struct | The blockwatcher-ports::Matcher implementation wrapping Predicate |
Registry, ExprConfig | struct | The ModuleRegistry registration for "expr"; ExprConfig is an empty, deny_unknown_fields struct, since the module takes no configuration of its own |
Ast, Node | struct/enum | The parsed tree: a Node paired with the byte Span it was parsed from |
BinOp | enum | The 17 binary operators the grammar admits |
Family | enum | The type-checker’s own, coarser type vocabulary (Int/Str/Bytes/Bool/Array/Map): Int collapses ValueType::Int/Uint, Bytes collapses Address/Bytes |
NodeType | struct | A 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) |
Truth | enum | The 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 schemasblockwatcher-ports: trait boundary for matcher portserde: (de)serialization deriveserde_json: JSON wire formatnum-bigint: arbitrary-precision integers
In [dev-dependencies]:
blockwatcher-expr(atpath = "."withtestingfeature): allowstests/corpus_replay.rsto reachtest_schemas::schemas()under barecargo testblockwatcher-ports(fakesfeature):FakeMatcherfor cross-checking inmatcher.rstestsindexmap: ordered mapsproptest: property testingtokio: 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; theblockwatcherbinary reaches this crate only through embed
Reading the source
- 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 bytests/dotted_field_addressing.rs). lexer.rs:Token,Span, andlex; token-decimal expansion and the two string escapes it recognizes.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.typecheck/mod.rs:Family,NodeType,families/bin_type(the per-operator-class admission rules), andcheck, the entry pointPredicate::compilecalls.typecheck/diagnostics.rs: every rejection message’s exact wording and the bounded edit-distance search behind the did-you-mean suggestion.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.eval.rs:Truth, the three-valued Kleeneevaluate, andexplain/explain_failed, the dry-run diagnostic walk.matcher.rs:ExprMatcher(theMatcherport implementation),Registry/ExprConfig(the"expr"module registration), andmatchers::get_all().test_schemas.rs: the schema fixture behind thetestingfeature, shared by this crate’s own tests, its proptest suites, its fuzz targets, andtests/corpus_replay.rs.