Aller au contenu

CompactaEVE and Matching Semantics

CompactaEVE is the terse compiled form of the EVE language, and the input the pattern compiler actually parses. Every ExpandaEVE construct lowers to a CompactaEVE symbol before it runs. This page documents the compact symbols, what the compiler accepts, and how a compiled pattern decides whether a candidate matches.

You do not have to write CompactaEVE by hand: the ExpandaEVE translator produces it. But reading it is useful for understanding exactly how a pattern behaves, and a document may be authored directly in the compact dialect.

The compact statement forms

The document-level directives keep the same names in lower case:

ExpandaEVE CompactaEVE
DEFINE name AS id define name as id
LET function(name) BE a OR b let &name be a\|b
REALM x realm x
HEAR n AS body / FIND n AS body hear n as body
MATCH n AS body match n as body
ANYWHERE n AS ... anywhere n as g1 !! g2
SEQUENCE n AS ... sequence n as ...

Class members and group arms are separated by the bar | rather than OR. A class is referenced by &name. The rest of this page covers the pattern body of a fuzzy rule, which is where the compact symbols live.

Root markers

Exactly one root marker separates the root stem from the rest of the pattern:

Symbol From ExpandaEVE Meaning
[^^] (WHOLE) The root matches as a whole word. The route must run to a word boundary.
[*^] (ROOT) The root is a stem that later parts build on.

The text before the marker is the root stem. ni[*^] has stem ni; spic[^^] has stem spic and requires a whole-word match.

Parts: the pieces of a fragment

A fragment (a root, a group arm, a suffix) is parsed into an ordered list of parts. The compiler recognizes these tokens; everything else accumulates as a literal run.

Token Kind Matches
plain text Literal The exact characters, in order.
@ Any Exactly one character of word material.
(#*) Gap A run of any characters, letters or not. Non-deterministic: it may end at any reachable position.
($^) Gap (letters) A run of letters (word material) only.
[swap:...] Swap Any one of several alternative arms with boundary rules.

A literal is matched exactly. @ consumes one word byte. A gap is the engine's tolerance for inserted characters: it can skip zero or more positions, and the matcher tries every reachable end.

Word material

Word material is an ASCII letter or any non-ASCII byte. A multi-byte glyph such as an emoji is a run of word bytes, so glyph runs tokenize and bound like words. No ASCII byte is treated as a symbol byte, so ordinary text is unchanged.

Roots and root alternatives

The root can take three forms:

  • A plain stem: masturb[*^] compiles to one root.
  • A class reference: &name... expands so each class member becomes a root alternative, with any trailing text kept.
  • A segmented root [[..][..]]: see Segmented roots.

Leading override brackets and prefix products generate more root alternatives:

  • [text;] before the root replaces the root's leading literal with text, producing an alternate spelling. This is what ALIAS lowers to. A [text^^;] or [text*^;] override also sets that alternative's boundary.
  • [+[a|b]] before the root is a prefix cross-product: each listed prefix is prepended to every root, multiplying the alternatives. This is what ALSO uses when it sits before the root, and what a class reference inside [+[...]] expands to.

Root alternatives are ordered longest-literal first, so a more specific spelling is tried before a shorter one. A root alternative is skipped early when a literal it must consume is absent from the candidate.

Segmented roots

A segmented root is written [[seg1][seg2]...] where a segment ending in * is an entry point. It generates alternatives that begin at each starred segment, optionally prefixed by the earlier segments. This lets a root like re[tard] be reached both as the whole word and from the inflectable segment. A segmented root with no starred entry point is an error.

Suffixes and endings (trailing)

Endings live in the trailing suffix list. In CompactaEVE they are added with |[a|b|...] after the root, which is what suffix(...) lowers to. A route matches only when the candidate ends on one of the trailing suffixes and runs to the end of the word.

An empty arm (NONE) makes the suffix optional.

Rewrite suffixes

A trailing arm written from>to is a rewrite. It matches a candidate that ends in to by stripping to, appending from, and testing the result against the core pattern. y>ies lets a root ending in y also match a word ending in ies, so one root reaches both baby and babies. The > marker is the only place a rewrite is interpreted, so every reader of trailing suffixes agrees on its meaning.

Groups

Between the root and the trailing suffixes the pattern is a sequence of groups. A group is a set of alternative arms, any one of which may satisfy it. A group has a kind:

Kind CompactaEVE Behaviour
Normal [a|b] Matched in sequence at the current position.
Plus (floating) [+[a|b]] Matched at any reachable slot after the root and the normal groups.
Hard (anchored) a hard class reference An anchored alternative matched from the start.

An arm that is empty makes its group optional. A group written as a bare gap, (#*) or ($^), is an optional gap group, which is how a standalone GAP or LETTERS between parts lowers.

Class references as groups

&name in the group position expands the class into a group of arms, one per member. &!name makes the group optional (an extra empty arm is added), which is how MAYBE FUNCTION lowers. A reference may carry [^^] or [*^] to force hard or soft matching; otherwise it inherits the class's declared hardness. A hard class reference becomes a hard anchored group.

Plus groups (floating)

A plus group [+[...]] does not sit in fixed order. After the root and the normal groups are consumed, the matcher computes every reachable position and lets the plus group attach at any of them, running to the end of the word. This is how ADDITIVE functions and ALSO float relative to the stem.

Swaps

[swap:a|b] matches any one arm at a position. Each arm carries boundary flags decided by its asterisks:

  • A single leading * frees the start boundary.
  • A single trailing * frees the end boundary.
  • No asterisk, or a double leading **, anchors both sides.

An anchored side requires a word boundary there: the start must follow a non-word byte (or be the string start), and the end must precede a non-word byte (or be the string end). A swap may be written inline in a fragment or as its own group.

Exclusions (veto)

[-[a|b]] is a veto list, which is what NEVER lowers to. Each listed phrase is a plain substring test against the whole line, lowercased. If any phrase is present, the pattern does not fire, regardless of the rest of the match.

Precheck and post gating

Two constructs gate a pattern on the surrounding line:

  • {pre:a|b} is a precheck, from BEFORE. The pattern stays inert unless one of the phrases is a substring of the line, lowercased.
  • {post:a|b} is a post condition, from AFTER. It is spliced in at the root marker as an optional leading group of allowed endings.

A gated pattern (one with a precheck) fires only when a precheck phrase is present in the line, or when the candidate satisfies the post condition on its own: it begins with the root stem and ends with one of the post endings, with a trailing plural s tolerated. The precheck and veto facts depend only on the line, not the candidate, so they are computed once per line and reused across candidates.

Skeletons

{skel:...} is the skeleton whitelist, from SKELETON. It is expanded at compile time into a set of concrete words. A candidate exactly equal to one of those words matches immediately, before the core matcher runs.

Inside a skeleton:

  • [a,b,c] is an inline choice list: one position that may be any comma-separated member. Members may be multi-character. Whitespace around a member is trimmed.
  • &name expands to the members of that class. A numeric class name expands digit by digit; a named class expands as letters.

The expansion is the full cross product of every choice, lowercased.

Flag wrappers

{name:y?...} and {name:n?...} wrap a pattern in a flag condition, from FLAG. The y form requires the flag on, the n form requires it off. Multiple wrappers may nest. The flag name must be declared with define, or the document fails to load. The declared flags are what the filter reports as a category.

Comments

{#c:...} is an inline comment, from NOTE. It is stripped before compilation, including nested braces. Whole-line comments use a leading #.

How a fuzzy pattern matches

When a candidate word is tested against a compiled fuzzy pattern, the engine runs these checks in order:

  1. Gating. If the pattern has a precheck and neither a precheck phrase is present in the line nor the post condition is satisfied, it does not match. If any veto phrase is present in the line, it does not match.
  2. Skeleton. If the candidate exactly equals a skeleton word, it matches.
  3. Core match. The main engine runs (below).
  4. Rewrite fallback. For each rewrite suffix, if the candidate ends in the to side, the from side is restored and the core match is retried on the restored word.

The core match

The core matcher decides whether the whole candidate is accounted for by a route:

  1. A fast reject skips the candidate when it lacks every literal that a core route must contain.
  2. Hard anchored groups are tried from the start; a route that reaches an allowed ending matches.
  3. Each root alternative is run from the start. A root whose required literals are absent is skipped. The reachable ends feed the sequence of normal groups.
  4. The normal groups are threaded in order from the root ends. A route matches when it reaches an allowed trailing suffix that runs to the end of the word. Unless the route is purely literal, at least one non-empty arm must have fired.
  5. Floating plus groups are tried at every reachable slot, and a plus group matches when one of its arms runs from a reachable anchor to the end of the word.

Word boundaries come from word material: a boundary exists at the string ends and wherever a non-word byte meets a word byte. Gaps skip runs of characters non-deterministically, so the matcher explores every position a gap can reach.

Anchors and the scan prefilter

For each fuzzy rule the compiler computes a set of required literal anchors: literals such that a candidate containing none of them cannot match. If a rule can match without any guaranteed literal (for example a purely gap or gated route), its anchor set is empty and the rule is always run. The anchors of every rule build an Aho-Corasick prefilter that, in one pass over a message, marks which rules could possibly match, so rules whose literals are absent are skipped. This is an optimization only: it never skips a rule that could match.

Strict rules (MATCH)

A MATCH rule uses the strict language rather than the fuzzy engine. A strict pattern is anchored: it succeeds only when its terms consume the entire input from start to finish. It is used for structured input like numbers and addresses, and it can capture named pieces.

Strict primitives

Keyword Matches
digit Exactly one digit.
digits A run of digits.
letter Exactly one letter.
letters A run of letters.
word A run of letters, digits, or underscore.
number A run of digits.
ip A run of digits and dots.
space Exactly one whitespace character.
spaces A run of whitespace.
anything Any stretch of text.

Keyword matching ignores case. A "..." token is a literal, with backslash escapes honoured.

Strict quantifiers and structure

Construct Effect
any x Zero or more of x, greedy.
many x One or more of x, greedy.
maybe x Zero or one of x.
m to n x Between m and n of x.
n digit (etc.) Exactly n of a class, for example 3 digit.
( ... ) A group matched as a sub-sequence.
x or y Alternation: either branch may satisfy the position.
then Optional readable glue between terms; ignored.
x as name Captures the matched text of x under name.

A quantifier applied to a plural class narrows it to its single-character form so it repeats one character at a time. Repetition is greedy only; there is no lazy mode, and an empty repeat cannot loop.

List references and fuzzy find in strict

&name in a strict pattern expands to an alternation of the class members as literals. A backtick term find `pattern` embeds a fuzzy EVE pattern: the strict matcher hands a non-space span to the compiled fuzzy pattern and accepts the span when it matches. Both need the registry, which is available when a document is compiled.

Anywhere and sequence rules

ANYWHERE and SEQUENCE are separate rule kinds. ANYWHERE fires when every group (separated by !! in compact form, WITH in ExpandaEVE) has a member found somewhere in the message; each member is compiled as a whole-word fuzzy matcher, and the rule needs at least two groups. SEQUENCE matches an ordered run of glyphs and is used by the symbol board, which is documented separately; a mirror prefix also accepts the reversed order, and the rule needs at least two glyphs.