# Matcher Architecture

`expect(...).to.be(...)` is built on top of a small `Matcher` role. The expected
value passed to `.be(...)` is either:

- **a plain value** — wrapped in the built-in `BeMatcher` (smartmatch), or
- **a `Matcher`-doing object** — used directly.

This is the seam every built-in matcher and every user-defined matcher plugs
into. If you want to add your own matcher without writing a class by hand, see
[Custom Matchers](custom-matchers.md) — `define-matcher` produces a matcher
that conforms to this same role.

## The Matcher role

```raku
unit module BDD::Behave::Matcher;

role Matcher is export {
  method matches($actual --> Bool) { ... }
  method failure-message($actual --> Str) { Str }
  method failure-message-negated($actual --> Str) { Str }
  method expected-value(--> Mu) { Nil }
  method description(--> Str) { self.^name }
}
```

| Method                             | Required? | Purpose                                                                                                                                 |
| ---------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `matches($actual)`                 | yes       | Return `True` / `False` for whether `$actual` matches.                                                                                  |
| `failure-message($actual)`         | no        | Message rendered when the expectation fails (positive form). Default: undefined `Str` (falls back to `Expected:` / `to be:` rendering). |
| `failure-message-negated($actual)` | no        | Message rendered when a `.not` expectation fails. Default: undefined `Str`.                                                             |
| `expected-value`                   | no        | The value stored in `Failure.expected` for tooling.                                                                                     |
| `description`                      | no        | Human-readable description, useful for error reporting and reflection.                                                                  |

## Where matcher classes live

Almost all users only ever write `use BDD::Behave;` — that pulls in `expect`, every built-in matcher, and the full DSL through the lazy-loading wrapper. Nothing here changes that.

You only need to know about specific submodules when you want to *instantiate* matcher classes directly — typically to compose them inside a custom matcher, or to unit-test a matcher with `Test`. Each family lives in its own submodule so unit tests can import just one and skip the rest:

| Submodule                          | Matcher classes                                                                                                                               |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `BDD::Behave::Matcher`             | The `Matcher` role itself.                                                                                                                    |
| `BDD::Behave::Matcher::Core`       | `BeMatcher`, `EqMatcher`                                                                                                                      |
| `BDD::Behave::Matcher::Collection` | `IncludeMatcher`, `ContainExactlyMatcher`, `StartWithMatcher`, `EndWithMatcher`, `AllMatcher`                                                 |
| `BDD::Behave::Matcher::Type`       | `BeAMatcher`, `BeAnInstanceOfMatcher`, `RespondToMatcher`, `HaveAttributesMatcher`                                                            |
| `BDD::Behave::Matcher::Numeric`    | `BeGreaterThanMatcher`, `BeGreaterThanOrEqualMatcher`, `BeLessThanMatcher`, `BeLessThanOrEqualMatcher`, `BeBetweenMatcher`, `BeWithinMatcher` |
| `BDD::Behave::Matcher::Boolean`    | `BeTruthyMatcher`, `BeFalsyMatcher`, `BeNilMatcher`                                                                                           |
| `BDD::Behave::Matcher::String`     | `MatchMatcher`                                                                                                                                |
| `BDD::Behave::Matcher::Exception`  | `RaiseErrorMatcher`                                                                                                                           |
| `BDD::Behave::Matcher::Change`     | `ChangeMatcher`                                                                                                                               |
| `BDD::Behave::Matcher::Async`      | `BeKeptMatcher`, `BeBrokenMatcher`, `CompleteWithinMatcher`, `EmitMatcher`, `EmitAtLeastMatcher`, `CompleteMatcher`, `EventuallyMatcher`       |

Each submodule already `use`s `BDD::Behave::Matcher` internally, but the role is *not* re-exported. If your code needs both, import both:

```raku
use BDD::Behave::Matcher;            # for `does Matcher`
use BDD::Behave::Matcher::Numeric;   # for BeBetweenMatcher, BeWithinMatcher, ...
```

## BeMatcher (built-in)

`BeMatcher` wraps Raku's smartmatch operator (`~~`). When you write:

```raku
expect(42).to.be(42);
expect('hello').to.be(/hell/);
expect(5).to.be(1..10);
expect($x).to.be(any(1, 2, 3));
```

…the runner constructs `BeMatcher.new(:expected(...))` under the hood. Because
`BeMatcher` deliberately leaves `failure-message` undefined, failure rendering
keeps the structured `Expected:` / `to be:` block plus the colorized `Diff:`
section described in [Diff Output](../diff/diff.md).

Because the underlying operator is `~~`, all four Raku junction kinds (`any`,
`all`, `one`, `none`) compose with `BeMatcher` directly — see
[Junctions](junctions.md).

## EqMatcher (built-in)

`eq` checks order-dependent structural equality using Raku's `eqv` operator.
It's invoked via `expect(...).to.eq(...)`:

```raku
expect([1, 2, 3]).to.eq([1, 2, 3]);            # passes
expect([1, 2, 3]).to.eq([3, 2, 1]);            # fails (order matters)

expect({ a => 1, b => 2 }).to.eq({ a => 1, b => 2 });  # passes
expect(42).to.eq(42);                          # passes
```

`eqv` is type-strict, so an `Array` is not equivalent to a `List` with the same
elements:

```raku
expect([1, 2, 3]).to.eq((1, 2, 3));            # fails (Array vs List)
```

`EqMatcher` deliberately leaves `failure-message` undefined, so failures fall
through to the structured `Expected:` / `to be:` block plus the colorized
`Diff:` section described in [Diff Output](../diff/diff.md).

Negation works the usual way:

```raku
expect([1, 2, 3]).to.not.eq([3, 2, 1]);
```

## ContainExactlyMatcher (built-in)

`contain-exactly` checks order-independent multiset equality on arrays / lists.
Each item in `actual` must correspond to one item in the expected list (matched
by `eqv`), with counts and totals matching:

```raku
expect([1, 2, 3]).to.contain-exactly(3, 1, 2);   # passes
expect([1, 1, 2]).to.contain-exactly(1, 2, 1);   # passes (multiset)
expect([1, 1, 2]).to.contain-exactly(1, 2);      # fails (counts differ)
expect([1, 2, 3]).to.contain-exactly(1, 2);      # fails (extra in actual)
expect([1, 2]).to.contain-exactly(1, 2, 3);      # fails (missing)
```

Items are passed as individual positional arguments. The slurp is
non-flattening, so passing a single array (`contain-exactly([1, 2])`) looks for
that array as one element. To spread an existing array, use `|@arr`:

```raku
my @want = 1, 2, 3;
expect([3, 2, 1]).to.contain-exactly(|@want);
```

The empty form passes for an empty array:

```raku
expect([]).to.contain-exactly();
```

Failure messages render as `expected <actual> to contain exactly <items>` (or
`not to contain exactly` under `.not`).

`match-array` is the array-form alias — it takes a single array argument and
delegates to the same matcher:

```raku
expect([1, 2, 3]).to.match-array([3, 2, 1]);   # passes
expect([1, 2, 3]).to.match-array([1, 2]);      # fails
```

`match-array` requires its argument to be an array / list; passing a scalar
dies with `match-array requires an array argument`.

## IncludeMatcher (built-in)

`include` checks membership across arrays, hashes, sets, bags, ranges, and
strings. It's invoked via `expect(...).to.include(...)`:

```raku
expect([1, 2, 3]).to.include(2);              # array element
expect([1, 2, 3]).to.include(1, 3);           # multiple elements (AND)
expect([[1, 2], [3, 4]]).to.include([1, 2]);  # nested element via eqv

expect({ a => 1, b => 2 }).to.include('a');         # hash key
expect({ a => 1, b => 2 }).to.include(a => 1);      # hash key + value
expect({ a => 1, b => 2 }).to.include(:a(1));       # named-pair shorthand

expect('hello world').to.include('world');    # string substring
expect('hello world').to.include('hello', 'world');

expect(set('a', 'b')).to.include('a');        # Set / Bag membership
expect(1..10).to.include(5);                  # Range membership
```

Multiple items are combined with AND semantics: every item must be present
for the matcher to pass. The slurp is non-flattening, so passing a single
array argument (`include([1, 2])`) looks for that array as one element rather
than spreading it. To spread an existing array, use `|@arr`.

Negation works the usual way:

```raku
expect([1, 2, 3]).to.not.include(99);
```

Failure messages render as `expected <actual> to include <items>` (or `not
to include` under `.not`).

### String-specific behavior

When the actual is a `Str`, `include` does substring matching via
`.contains`. A few details worth knowing:

```raku
expect('').to.include('');                     # empty needle matches everything
expect("col1\tcol2").to.include("\t");         # whitespace and control chars
expect("a\nb").to.include("a\nb");             # substrings spanning newlines
expect('café au lait').to.include('café');     # Unicode (ASCII extension)
expect('日本語のテスト').to.include('日本');     # CJK
expect('done 🚀').to.include('🚀');            # emoji
expect('order #42').to.include(42);            # non-Str args are .Str-coerced
```

Matching is case sensitive — `expect('Hello').to.include('hello')` fails.
For case-insensitive matching, use [`MatchMatcher`](#matchmatcher-built-in)
with a regex like `rx:i/hello/`.

Allomorphs (`IntStr`, `RatStr`, `<42>`) are accepted as actuals and routed
through the `Str` branch since they smartmatch as `Str`.

## StartWithMatcher (built-in)

`start-with` checks that a sequence (Array, List) begins with the supplied
items, or that a string begins with each supplied prefix:

```raku
expect([1, 2, 3]).to.start-with(1);            # passes
expect([1, 2, 3]).to.start-with(1, 2);         # passes (in-order prefix)
expect([1, 2, 3]).to.start-with(2);            # fails
expect([1, 2, 3]).to.start-with(2, 1);         # fails (out of order)
expect([1]).to.start-with(1, 2);               # fails (prefix longer)

expect('hello world').to.start-with('hello');         # passes
expect('hello world').to.start-with('hello', 'h');    # passes (each prefix AND)
expect('hello world').to.start-with('hello', 'world');# fails ('world' is not a prefix)
```

For `Positional` / `Iterable` actuals, the args form an in-order prefix matched
via `eqv`. For `Str` actuals, each arg must independently be a prefix of the
string (AND semantics).

The slurp is non-flattening, so passing a single array (`start-with([1, 2])`)
looks for that array as one prefix element. To spread an existing array, use
`|@arr`.

`start-with` rejects undefined or non-iterable, non-string actuals. Empty arg
lists die with `start-with requires at least one item`.

Failure messages render as `expected <actual> to start with <items>` (or `not
to start with` under `.not`).

### String-specific behavior

When the actual is a `Str`, each prefix is checked via `.starts-with` after
`.Str` coercion. Multi-prefix calls AND together — every supplied prefix must
independently be a prefix of the string:

```raku
expect('').to.start-with('');                  # empty prefix matches everything
expect("\thello").to.start-with("\t");         # leading tab / control char
expect("a\nb").to.start-with("a\nb");          # prefix can cross a newline
expect('café au lait').to.start-with('café');  # Unicode (ASCII extension)
expect('日本語').to.start-with('日本');         # CJK
expect('🚀 launch').to.start-with('🚀');       # emoji
expect('42 answers').to.start-with(42);        # non-Str args are .Str-coerced
```

Matching is case sensitive and respects leading whitespace —
`expect('hello').to.start-with(' hello')` fails. Allomorphs (`IntStr`,
`RatStr`) are accepted as actuals and routed through the `Str` branch.

## EndWithMatcher (built-in)

`end-with` mirrors `start-with` for the trailing end of a sequence or string:

```raku
expect([1, 2, 3]).to.end-with(3);              # passes
expect([1, 2, 3]).to.end-with(2, 3);           # passes (in-order suffix)
expect([1, 2, 3]).to.end-with(2);              # fails
expect([1, 2, 3]).to.end-with(3, 2);           # fails (out of order)
expect([1]).to.end-with(1, 2);                 # fails (suffix longer)

expect('hello world').to.end-with('world');           # passes
expect('hello world').to.end-with('world', 'd');      # passes (each suffix AND)
expect('hello world').to.end-with('world', 'hello');  # fails ('hello' is not a suffix)
```

Same slurp / type / empty-arg conventions as `start-with`. Failure messages
render as `expected <actual> to end with <items>` (or `not to end with` under
`.not`).

### String-specific behavior

When the actual is a `Str`, each suffix is checked via `.ends-with` after
`.Str` coercion. Multi-suffix calls AND together — every supplied suffix must
independently be a suffix of the string:

```raku
expect('').to.end-with('');                    # empty suffix matches everything
expect("done\n").to.end-with("\n");            # trailing newline / control char
expect("a\nb").to.end-with("a\nb");            # suffix can cross a newline
expect('le café').to.end-with('café');         # Unicode (ASCII extension)
expect('日本語').to.end-with('語');             # CJK
expect('moon 🚀').to.end-with('🚀');           # emoji
expect('answer = 42').to.end-with(42);         # non-Str args are .Str-coerced
```

Matching is case sensitive and respects trailing whitespace —
`expect('hello').to.end-with('hello ')` fails. Allomorphs (`IntStr`, `RatStr`)
are accepted as actuals and routed through the `Str` branch.

## AllMatcher (built-in)

`all` checks that **every element** of a collection matches an inner matcher.
The inner argument is either a plain value (wrapped in `BeMatcher`, smartmatch
semantics) or any object that does `Matcher`:

```raku
expect([1, 1, 1]).to.all(1);                # plain value via BeMatcher
expect([1, 2, 3]).to.all(Int);              # type
expect([1, 5, 10]).to.all(1..10);           # range
expect(['foo', 'food']).to.all(/^foo/);     # regex

expect([1, 2, 3]).to.all(PositiveMatcher.new);          # custom matcher
expect([[1, 2], [1, 3]]).to.all(                        # composes with built-ins
  StartWithMatcher.new(:expected([1]))
);
```

An empty collection passes vacuously:

```raku
expect([]).to.all(Int);                     # passes
expect(()).to.all(1);                       # passes
```

Undefined or non-iterable actuals fail with a shape failure message
(`expected ... to be a collection ...`). For sequence actuals, the matcher
iterates `$actual.list`, so `Hash` actuals are iterated as `Pair`s.

Failure messages render as
`expected <actual> to all <inner-description> (element at index N: <item> did not match)`,
pointing at the **first** failing element. Negation renders as
`expected <actual> not to all <inner-description>`.

### Composing across collections of collections

`all` is most useful when the inner matcher is itself a structural matcher:

```raku
my @rows = [
  { id => 1, status => 'ok' },
  { id => 2, status => 'ok' },
];

expect(@rows).to.all(IncludeMatcher.new(:expected([status => 'ok'])));
```

### Junctions

Pass junctions through `.all(...)` directly — the method binds its argument
raw so junctions are not autothreaded out:

```raku
expect([1, 2, 3]).to.all(any(1, 2, 3));
```

## BeAMatcher (built-in)

`be-a` checks whether the actual value is "of" a given type, including
subclasses, composed roles, and `subset` types. Internally it smartmatches
the actual value against the type (`$actual ~~ $type`):

```raku
expect(42).to.be-a(Int);              # passes
expect(42).to.be-a(Numeric);          # passes (Int is Numeric)
expect('hi').to.be-a(Int);            # fails

class Animal {}
class Dog is Animal {}
expect(Dog.new).to.be-a(Animal);      # passes (subclass)

role Walkable { method walk { } }
class Bird does Walkable {}
expect(Bird.new).to.be-a(Walkable);   # passes (role composition)

subset Positive of Int where * > 0;
expect(5).to.be-a(Positive);          # passes
expect(-1).to.be-a(Positive);         # fails (where clause)
```

`be-an` is provided as an alias for English grammar — it behaves identically:

```raku
expect(42).to.be-an(Int);
```

Failure messages render as `expected <actual> to be a <type>` (or `not to be
a <type>` under `.not`). The type name comes from `$type.^name`.

## BeAnInstanceOfMatcher (built-in)

`be-an-instance-of` is a **strict** type check: the actual value's runtime
type must be exactly the given type. Subclasses, composed roles, and subsets
do not match. Internally it checks `$actual.defined && $actual.WHAT === $type`:

```raku
expect(Dog.new).to.be-an-instance-of(Dog);      # passes
expect(Dog.new).to.be-an-instance-of(Animal);   # fails (subclass)
expect(42).to.be-an-instance-of(Int);           # passes
expect(42).to.be-an-instance-of(Numeric);       # fails (parent role)
```

Type objects (the uninstantiated type itself) do not match:

```raku
expect(Int).to.be-an-instance-of(Int);          # fails (Int is undefined)
```

Because composed roles and subsets are not the runtime type of any concrete
object, `be-an-instance-of(SomeRole)` and `be-an-instance-of(SomeSubset)`
always fail. Use `be-a` for those cases.

Failure messages render as `expected <actual> to be an instance of <type>`
(or `not to be an instance of <type>` under `.not`).

## RespondToMatcher (built-in)

`respond-to` checks whether the actual value has one or more methods.
Internally it uses the meta-object protocol's `^can` introspection
(`$actual.^can($name)`), so it works on both instances and type objects,
and recognises methods supplied by composed roles and parent classes.

```raku
class Calculator {
  method add($a, $b) { $a + $b }
  method subtract($a, $b) { $a - $b }
}

expect(Calculator.new).to.respond-to('add');                # passes
expect(Calculator.new).to.respond-to('add', 'subtract');    # passes
expect(Calculator.new).to.respond-to('multiply');           # fails

role Greeter { method greet { 'hello' } }
class Person does Greeter { }
expect(Person.new).to.respond-to('greet');                  # passes (via role)

expect('hello').to.respond-to('uc', 'lc', 'chars');         # built-ins work
expect([1, 2, 3]).to.respond-to('push', 'pop');             # Arrays too
```

Multiple method names are AND-combined — every name must be present for
the expectation to pass. When the expectation fails, the failure message
lists the missing methods:

```text
expected Dog.new to respond to "bark", "meow", "purr" (missing: "meow", "purr")
```

Negation works the usual way:

```raku
expect(Dog.new).to.not.respond-to('meow');
```

Failure messages render as `expected <actual> to respond to <names>`
(or `not to respond to <names>` under `.not`).

## HaveAttributesMatcher (built-in)

`have-attributes` checks several attributes of an object in one call.
Each named pair maps an attribute name to an expected value (or to
another `Matcher`). For each pair, the matcher calls the accessor on
the actual value and compares — values are compared with `eqv`, and
when the expected side is itself a `Matcher` its `matches` method is
delegated to.

```raku
class Person {
  has Str $.name;
  has Int $.age;
}

my $alice = Person.new(:name<Alice>, :age(30));

expect($alice).to.have-attributes(:name<Alice>, :age(30));     # passes
expect($alice).to.have-attributes(:name<Alice>);               # subset OK
expect($alice).to.have-attributes(:age(31));                   # fails
```

Multiple attributes are AND-combined — every pair must match. When the
expectation fails, the failure message separates *missing* accessors
(the object does not respond to the name at all) from *mismatched*
values (accessor exists, but the value disagrees):

```text
expected Person.new(name => "Alice", age => 30) to have attributes age => 31, nickname => "Al" (missing: "nickname"; mismatched: age: got 30, wanted 31)
```

The matcher composes naturally with other matchers — pass a `Matcher`
instance as the expected value for any attribute:

```raku
use BDD::Behave::Matcher::Collection;   # StartWithMatcher
use BDD::Behave::Matcher::Type;         # BeAMatcher

expect($alice).to.have-attributes(
  :name(StartWithMatcher.new(:expected(['A']))),
  :age(BeAMatcher.new(:type(Int))),
);
```

Negation works the usual way:

```raku
expect($alice).to.not.have-attributes(:age(31));
```

Failure messages render as `expected <actual> to have attributes
<pairs>` (or `not to have attributes <pairs>` under `.not`).

## Comparison matchers (built-in)

Four matchers cover numeric ordering:

| Matcher                       | Operator | Aliases  |
| ----------------------------- | -------- | -------- |
| `be-greater-than`             | `>`      | `be-gt`  |
| `be-greater-than-or-equal-to` | `>=`     | `be-gte` |
| `be-less-than`                | `<`      | `be-lt`  |
| `be-less-than-or-equal-to`    | `<=`     | `be-lte` |

```raku
expect(5).to.be-greater-than(3);
expect(5).to.be-greater-than-or-equal-to(5);
expect(3).to.be-less-than(5);
expect(5).to.be-less-than-or-equal-to(5);

expect(5).to.be-gt(3);
expect(5).to.be-gte(5);
expect(3).to.be-lt(5);
expect(5).to.be-lte(5);
```

All four accept any `Real` value, so `Int`, `Rat`, and `Num` mix freely
and negatives / zero behave as expected:

```raku
expect(1.5).to.be-greater-than(1.4);     # Rat vs Rat
expect(5).to.be-greater-than(4.99);      # Int vs Rat
expect(3.14e0).to.be-greater-than(3.0e0); # Num vs Num
expect(-1).to.be-greater-than(-5);
expect(0).to.not.be-greater-than(0);
```

Each matcher fails (rather than dying) when `$actual` is undefined or
is not a `Real`, so a stray `Int`-typed `Nil` or a `Str` produces a
recorded failure instead of a runtime error:

```raku
expect(Int).to.be-greater-than(0);  # records a failure
expect('abc').to.be-less-than(10);  # records a failure
```

Negation works the usual way:

```raku
expect(3).to.not.be-greater-than(5);
expect(7).to.not.be-less-than(5);
```

Failure messages render as `expected <actual> to be greater than
<expected>` (and the obvious variants for the other three), or `not to
be …` under `.not`.

## BeBetweenMatcher (built-in)

`be-between` checks whether a numeric `actual` falls within a `[min, max]`
range. The matcher is inclusive by default; chain `.exclusive` (or the
explicit `.inclusive`) to flip the mode:

```raku
expect(5).to.be-between(1, 10);              # inclusive (default), passes
expect(1).to.be-between(1, 10);              # inclusive: lower bound passes
expect(10).to.be-between(1, 10);             # inclusive: upper bound passes

expect(5).to.be-between(1, 10).exclusive;    # exclusive: passes strictly inside
expect(1).to.be-between(1, 10).exclusive;    # exclusive: lower bound fails
expect(10).to.be-between(1, 10).exclusive;   # exclusive: upper bound fails

expect(5).to.be-between(1, 10).inclusive;    # equivalent to the default
```

All four call sites use the same chainable expectation. Re-chaining a
modifier *replaces* any previously recorded failure, so
`expect(10).to.be-between(1, 10).exclusive.inclusive` ends with no
failure: the `.exclusive` step pushes one, then `.inclusive` clears it
when the re-evaluation passes.

`be-between` accepts any `Real` actual, so `Int`, `Rat`, and `Num` mix
freely and negative / zero bounds behave the obvious way:

```raku
expect(1.5).to.be-between(1.0, 2.0);
expect(2).to.be-between(1.5, 2.5);
expect(-3).to.be-between(-5, -1);
expect(0).to.be-between(0, 0);
```

The matcher fails (rather than dying) when `$actual` is undefined or is
not a `Real`, so a stray `Int`-typed `Nil` or a `Str` produces a
recorded failure instead of a runtime error:

```raku
expect(Int).to.be-between(0, 10);            # records a failure
expect('abc').to.be-between(0, 10);          # records a failure
```

Negation works the usual way, and composes with the inclusive / exclusive
modifiers:

```raku
expect(0).to.not.be-between(1, 10);
expect(1).to.not.be-between(1, 10).exclusive;
```

Failure messages name both bounds and the active mode:

```text
expected 11 to be between 1 and 10 (inclusive)
expected 10 to be between 1 and 10 (exclusive)
```

`Failure.expected` is populated as `[min, max]` so programmatic
consumers and alternate formatters can introspect the bounds.

## BeWithinMatcher (built-in)

`be-within` performs a tolerance check on a numeric `actual` against an
expected target, parameterized by a `delta`. The call shape uses an
`.of(...)` continuation so the delta and expected target read naturally:

```raku
expect(5.05).to.be-within(0.1).of(5.0);     # passes (|5.05 - 5.0| <= 0.1)
expect(5.1).to.be-within(0.1).of(5.0);      # passes (boundary is inclusive)
expect(5.2).to.be-within(0.1).of(5.0);      # fails
expect(3.14e0).to.be-within(0.01e0).of(3.15e0);  # Num tolerance
```

The boundary is inclusive — `abs(actual - expected) <= delta` — so a
delta of `0` means actual must equal expected exactly:

```raku
expect(5).to.be-within(0).of(5);            # passes
expect(5.0001).to.be-within(0).of(5);       # fails
```

`be-within` accepts any `Real` actual and target, so `Int`, `Rat`, and
`Num` mix freely. Negative values and zero behave the obvious way:

```raku
expect(-5.05).to.be-within(0.1).of(-5.0);
expect(0.05).to.be-within(0.1).of(0);
```

The matcher fails (rather than dying) when either `$actual` or the
target passed to `.of(...)` is undefined or non-`Real`, so a stray
`Int`-typed `Nil` or a `Str` produces a recorded failure instead of a
runtime error:

```raku
expect(Int).to.be-within(0.1).of(5.0);      # records a failure
expect('abc').to.be-within(0.1).of(5.0);    # records a failure
expect(5.0).to.be-within(0.1).of(Int);      # records a failure
```

Negation works the usual way:

```raku
expect(5.2).to.not.be-within(0.1).of(5.0);  # passes (outside tolerance)
expect(5.05).to.not.be-within(0.1).of(5.0); # fails (inside tolerance)
```

Failure messages render the delta and expected target:

```text
expected 5.2 to be within 0.1 of 5.0
expected 5.05 not to be within 0.1 of 5.0
```

`Failure.expected` is populated with the expected target (not the
delta), and `Failure.given` holds the actual value, so programmatic
consumers and alternate formatters can introspect both.

## BeTruthyMatcher (built-in)

`be-truthy` checks Raku's boolean coercion of the actual value (`?$actual`).
Anything that coerces to `True` passes; anything that coerces to `False`
fails:

```raku
expect(True).to.be-truthy;
expect(1).to.be-truthy;
expect('hello').to.be-truthy;
expect([1, 2, 3]).to.be-truthy;
expect({ a => 1 }).to.be-truthy;

expect(False).to.not.be-truthy;
expect(0).to.not.be-truthy;
expect('').to.not.be-truthy;
expect([]).to.not.be-truthy;
expect(Nil).to.not.be-truthy;
expect(Int).to.not.be-truthy;        # undefined type object
```

Raku's coercion may surprise users coming from Perl: non-empty strings
are always truthy (`'0'.Bool` is `True`), but an empty `Array` / `Hash`
is `False`:

```raku
expect('0').to.be-truthy;        # non-empty string in Raku
expect([]).to.not.be-truthy;
expect({}).to.not.be-truthy;
```

`be-truthy` takes no arguments. Negation works the usual way:

```raku
expect(0).to.not.be-truthy;
```

Failure messages render as `expected <actual> to be truthy` (or `not to
be truthy` under `.not`).

## BeFalsyMatcher (built-in)

`be-falsy` is the inverse of `be-truthy` — it passes when `!$actual` is
`True`:

```raku
expect(False).to.be-falsy;
expect(0).to.be-falsy;
expect('').to.be-falsy;
expect([]).to.be-falsy;
expect({}).to.be-falsy;
expect(Nil).to.be-falsy;
expect(Int).to.be-falsy;            # undefined type object

expect(True).to.not.be-falsy;
expect(1).to.not.be-falsy;
expect('hello').to.not.be-falsy;
expect([1, 2, 3]).to.not.be-falsy;
```

`be-falsy` takes no arguments. Failure messages render as
`expected <actual> to be falsy` (or `not to be falsy` under `.not`).

## BeNilMatcher (built-in)

`be-nil` passes when the actual value is undefined (`!$actual.defined`).
That covers Raku's three flavors of "not a value":

- `Nil` itself
- `Any` (the default for an unassigned scalar)
- any type object, including built-ins like `Int`/`Str` and user-defined classes

```raku
expect(Nil).to.be-nil;
expect(Any).to.be-nil;
expect(Int).to.be-nil;          # undefined built-in type object
expect(Str).to.be-nil;

my class Widget {}
expect(Widget).to.be-nil;       # user-defined type object
expect(Widget.new).to.not.be-nil; # defined instance
```

Defined values — even "empty" or falsy ones — fail `be-nil`:

```raku
expect(0).to.not.be-nil;
expect('').to.not.be-nil;
expect([]).to.not.be-nil;
expect({}).to.not.be-nil;
expect(False).to.not.be-nil;
```

This is the type-object-vs-instance distinction that Raku makes
explicit: `Int` (the type) is undefined, but `0` (an instance of
`Int`) is defined. Use `be-nil` when you care about "is this a real
value", and `be-falsy` when you care about boolean coercion (which
treats empty collections as false too).

Note that assigning `Nil` to a plain `my $x` reverts `$x` to `Any` —
both still pass `be-nil`, so the distinction rarely matters in
practice.

`be-nil` takes no arguments. Failure messages render as
`expected <actual.raku> to be nil` (or `not to be nil` under `.not`).

## MatchMatcher (built-in)

`match` passes when a `Str` actual value smartmatches against a `Regex`
expected value (`$actual ~~ /pattern/`). It accepts the full Raku regex
syntax, including character classes, alternation, anchors, modifiers,
and `rx//`-quoted forms.

```raku
expect('abc123').to.match(/\d+/);
expect('hello world').to.match(/world/);
expect('HELLO').to.match(rx:i/hello/);     # case-insensitive
expect('hello').to.match(/^hello$/);       # anchored
```

Negation uses the same `.not` chain:

```raku
expect('abc').to.not.match(/\d+/);
expect('cat').to.not.match(/dog/);
```

Undefined and non-`Str` actuals fail rather than throw — `expect(Any)`,
`expect(Str)`, `expect(42)`, and `expect([1, 2, 3])` all record a
failure when matched against any regex, so a stray nil or wrongly typed
value produces a normal expectation failure instead of a runtime
exception.

Failure messages render the actual value and the regex via `.raku`:

```
expected "abc" to match /\d+/
expected "abc123" not to match /\d+/
```

`Failure.given` carries the original string and `Failure.expected`
carries the `Regex` itself, so programmatic consumers and alternate
formatters can inspect either side.

`match` is regex-only; for substring checks use `include` (see
[Matchers › IncludeMatcher](matchers.md#includematcher-built-in)).

## RaiseErrorMatcher (built-in)

`raise-error` passes when the actual value is a `Callable` and invoking it
raises an exception. Because the matcher operates on a `Callable`, the
block must be passed unevaluated — wrap the code under test in `{ ... }`:

```raku
expect({ die "boom" }).to.raise-error;
expect({ X::AdHoc.new(payload => 'x').throw }).to.raise-error;
expect({ 1 + 1 }).to.not.raise-error;
```

### Filtering by exception type

Pass an exception type as the first argument to require that the raised
exception is *that* type, a subclass of it, or a class that does it as a
role (matching uses smartmatch, i.e. `$exception ~~ $type`):

```raku
class X::Demo is Exception { method message { 'demo' } }
class X::Sub  is X::Demo  { }

expect({ X::Demo.new.throw }).to.raise-error(X::Demo);   # passes
expect({ X::Sub.new.throw  }).to.raise-error(X::Demo);   # passes (subclass)
expect({ die "boom"        }).to.raise-error(X::Demo);   # fails
```

When the block raises a *different* type, the failure surfaces what
actually happened:

```
expected block to raise X::Demo, but raised X::AdHoc: boom
```

When the block does not raise at all under a typed expectation:

```
expected block to raise X::Demo, but none was raised
```

`Failure.expected` carries the expected type, so programmatic consumers
and alternate formatters can read it directly.

### Filtering by message pattern

Pass a `Regex` (with or without a leading type) to require that the
raised exception's `.message` matches the regex:

```raku
expect({ die "code=42"            }).to.raise-error(/'code=42'/);
expect({ X::Demo.new.throw        }).to.raise-error(X::Demo, /demo/);
expect({ X::Demo.new.throw        }).to.raise-error(X::Demo, /nope/);  # fails
```

When the type matches but the message does not (or vice versa), the
failure says what was raised so the mismatch is diagnosable inline:

```
expected block to raise X::Demo with message matching /nope/,
  but raised X::Demo: demo
```

### Filtering by message via `with-message`

`raise-error` also supports a chained `.with-message(...)` form that
filters by either an exact `Str` or a `Regex`. The chain is equivalent
to passing the regex inline but reads more naturally when the message
constraint is a string:

```raku
expect({ die "boom" }).to.raise-error.with-message('boom');         # exact
expect({ die "code=42" }).to.raise-error.with-message(/'code=42'/); # regex
expect({ X::Demo.new.throw }).to.raise-error(X::Demo).with-message('demo');
```

`Str` arguments compare with `eq` (the full message must match
exactly); `Regex` arguments compare with `~~`. The chain may be
re-applied — only the most recent call's expectation is in effect, and
any failure recorded by an earlier link in the chain is replaced rather
than appended:

```raku
my $exp = expect({ die "boom" }).to.raise-error.with-message('bang');
# Failures.list has one entry.
$exp.with-message('boom');
# Failures.list is now empty — the second call succeeded and cleared
# the prior failure.
```

When the chain ultimately fails, the failure message reflects the
final constraint:

```
expected block to raise an error with message "bang", but raised X::AdHoc: boom
expected block to raise X::Demo with message "demo", but raised X::AdHoc: oops
```

`.with-message` also composes with `.not`:

```raku
expect({ die "boom" }).to.not.raise-error.with-message('bang');  # passes
expect({ die "boom" }).to.not.raise-error.with-message('boom');  # fails
```

### Negation

`.not.raise-error(...)` passes whenever the block fails to match the
*full* filter — i.e. nothing was raised, or a different type was raised,
or the message did not match. It only fails when the block raised
exactly the forbidden exception:

```raku
expect({ die "boom" }).to.not.raise-error(X::Demo);   # passes (other type)
expect({ 1 + 1 }).to.not.raise-error(X::Demo);        # passes (no raise)
expect({ X::Demo.new.throw }).to.not.raise-error(X::Demo);   # fails
```

When `.not.raise-error` fails, the negated failure includes the raised
exception's type and message so you can diagnose without re-running:

```
expected block not to raise an error, but one was raised (X::AdHoc: boom)
expected block not to raise X::Demo, but one was raised (X::Demo: demo)
```

### Non-Callable actuals

Non-`Callable` actuals fail rather than throw — `expect(42).to.raise-error`
records a normal expectation failure rather than blowing up. The failure
message names the unwrapped value so the mistake is obvious:

```
expected a Callable for raise-error, but got 42
```

### Failure metadata

`Failure.given` carries the original `Callable`. `Failure.expected`
carries the expected exception type (when one was supplied), or the
expected message (regex or `Str`, whichever was supplied via the inline
form or `with-message`); for the bare no-argument form it remains
unset. The matcher itself also exposes `raised-exception` (the captured
throw) and `miss-reason` (one of `'none'`, `'type'`, `'message'`,
`'non-callable'`, or `Str` on a pass) for tooling that needs structural
access.

## ChangeMatcher (built-in)

`change` passes when invoking the actual block changes the value
returned by an observable block. Both the action and the observable
are passed unevaluated as `{ ... }` blocks — the matcher invokes the
observable, runs the action, then invokes the observable again and
compares the two values via `eqv`:

```raku
my $counter = 0;
expect({ $counter++ }).to.change({ $counter });        # passes

my @items;
expect({ @items.push: 'x' }).to.change({ @items.elems });  # passes

my $counter2 = 5;
expect({ 1 + 1 }).to.change({ $counter2 });            # fails (unchanged)
```

The observable can be any expression — a variable, a method call, a
derived value — as long as the block returns something comparable. The
matcher uses `eqv` for the before / after comparison, so deep
structural equality is respected: mutating an array element to the
same value (`@items[0] = 1` when it was already `1`) does **not**
register as a change.

### Capturing snapshots

Because `eqv` compares the values *returned* by the observable each
time, an observable like `{ @items }` returns the *same container* on
both calls. To detect element-level mutation, snapshot the value:

```raku
my @items = 1, 2, 3;
expect({ @items.push: 4 }).to.change({ @items.clone });   # passes
expect({ @items.push: 5 }).to.change({ @items.elems });   # passes (count)
```

### Negation

`.not.change` passes when the action does **not** change the
observable, and fails when it does:

```raku
my $counter = 5;
expect({ 1 + 1 }).to.not.change({ $counter });        # passes

expect({ $counter = 9 }).to.not.change({ $counter });
# fails: expected block not to change observable, but it changed from 5 to 9
```

### Non-Callable actuals

Non-`Callable` actuals fail rather than throw — `expect(42).to.change({ $x })`
records a normal expectation failure rather than blowing up. The
failure message names the unwrapped value:

```text
expected a Callable for change, but got 42
```

### Filtering with `.from` and `.to`

`change` returns a chainable expectation that can be narrowed with
`.from(value)` and `.to(value)` to assert the precise transition:

```raku
my $counter = 0;
expect({ $counter = 10 }).to.change({ $counter }).from(0).to(10);

# .from alone — must start at 0 *and* change
my $a = 0;
expect({ $a++ }).to.change({ $a }).from(0);

# .to alone — must end at 10 *and* differ from the start
my $b = 0;
expect({ $b = 10 }).to.change({ $b }).to(10);

# .to.from order is interchangeable
my $c = 1;
expect({ $c = 2 }).to.change({ $c }).to(2).from(1);
```

`.from` and `.to` use `eqv` for the comparison, so deep structural
equality is respected (e.g. `.from([])` matches an empty-array
snapshot). The action block is run exactly once per `.change(...)`
call — chaining `.from(...).to(...)` after a `.change(...)` does
not re-run it.

When a chain step fails and a later step passes, the prior failure
is replaced (rather than duplicated) so the recorded `Failure` list
always reflects the final state.

### Negation with `.from` / `.to`

The chain participates in the existing `.not` flow with conjunction
semantics: `.not.change(...).from(x).to(y)` passes when the joint
constraint (change AND from match AND to match) does *not* hold.
This means the block may still have changed the observable — just
not in that particular direction. The negated failure message
includes the clause for clarity:

```raku
my $counter = 0;
expect({ $counter = 7 }).to.not.change({ $counter }).from(0).to(10);
# passes — the value changed, but not to 10

expect({ $counter = 10 }).to.not.change({ $counter }).from(0).to(10);
# fails: expected block not to change observable from 0 to 10,
#        but it changed from 0 to 10
```

### Filtering with `.by` / `.by-at-least` / `.by-at-most`

For numeric observables, `.by(delta)` asserts the precise signed
change, while `.by-at-least(delta)` and `.by-at-most(delta)` bound
the change from below and above:

```raku
my $counter = 0;
expect({ $counter += 5 }).to.change({ $counter }).by(5);

# negative delta
my $balance = 100;
expect({ $balance -= 25 }).to.change({ $balance }).by(-25);

# bounded range
my $score = 0;
expect({ $score += 5 }).to.change({ $score })
  .by-at-least(1).by-at-most(10);
```

All three accept any `Real` (`Int`, `Rat`, `Num`); the matcher
subtracts `after - before` and compares against the expected delta
with the usual numeric operators. If the observable returns a
non-numeric value (e.g. a `Str`), the matcher records a
`by-non-numeric` failure rather than coercing.

The by-modifiers compose with `.from(...)` and `.to(...)`. From
and to mismatches still take priority — `.from(0).by(5)` reports
a from-mismatch when the value did not start at 0, even if the
delta would otherwise be wrong. RSpec parity: when the value did
not change at all, the no-change miss-reason fires first, so
`.by(0)` is effectively unreachable.

When a chain step fails and a later step passes, the prior
failure is replaced (rather than duplicated) so the recorded
`Failure` list always reflects the final state.

### Failure messages

```text
expected block to change observable, but it remained 7
expected block not to change observable, but it changed from 0 to 9
expected block to change observable from 0, but it started as 3
expected block to change observable to 10, but it ended as 7
expected block to change observable from 0 to 10, but it started as 3
expected block to change observable from 0 to 10, but it ended as 7
expected block to change observable from 5 to 5, but it remained 5
expected block to change observable by 5, but it changed by 3
expected block to change observable by at least 5, but it changed by 3
expected block to change observable by at most 5, but it changed by 7
expected a Callable for change, but got 42
```

### Failure metadata

`Failure.given` carries the original action `Callable`.
`Failure.expected` reflects the most-specific constraint
supplied: `[from, to]` when both endpoints are set, the from / to
value alone when only one is set, the by / by-at-least /
by-at-most value when only a delta constraint is set, or `Nil`
for the bare form. The matcher itself exposes `before-value`,
`after-value`, `action-ran`, `callable-given`, and `miss-reason`
(one of `'non-callable'`, `'from'`, `'to'`, `'no-change'`,
`'by'`, `'by-at-least'`, `'by-at-most'`, `'by-non-numeric'`, or
`Str` on a pass) for tooling that needs structural access to the
captured states. `delta` returns `after - before` when both
values are `Real`, and `Nil` otherwise.

## BeKeptMatcher (built-in)

`be-kept` waits for a `Promise` to settle and passes when it ends in the
`Kept` state. It blocks up to a configurable timeout (default 5 seconds)
using `Promise.anyof(...)` so it never hangs forever:

```raku
expect(Promise.kept('done')).to.be-kept;          # passes immediately
expect(start { compute() }).to.be-kept;           # passes when start { } finishes
expect(Promise.broken('oops')).to.not.be-kept;    # passes; broken is not kept
expect($pending).to.be-kept(0.5);                 # custom timeout in seconds
```

If the promise broke, the failure message includes the cause's
class and message so you can see what went wrong without rerunning:

```
expected Promise to be kept, but it was broken (X::AdHoc: boom)
```

If the timeout elapsed with the promise still pending, the message
quotes the timeout:

```
expected Promise to be kept within 0.05s, but it was still pending
```

The matcher exposes `value` (set when the promise was kept),
`cause` (set when the promise was broken), `timed-out`, and
`promise-given` for tooling.

`be-kept` accepts a positional `Real` timeout in seconds; pass `0.5`
for 500 ms. Non-Promise actuals fail with a Promise-shape message
(`expected a Promise for be-kept, but got 42`) rather than dying.

## BeBrokenMatcher (built-in)

`be-broken` is the mirror of `be-kept`: it waits up to the timeout
(default 5 seconds) and passes when the promise ends in the
`Broken` state. The captured cause is exposed on the matcher's
`cause` accessor:

```raku
expect(Promise.broken('boom')).to.be-broken;
expect(start { die 'eventual failure' }).to.be-broken;
expect(Promise.kept('happy')).to.not.be-broken;
expect($pending).to.be-broken(0.5);               # custom timeout
```

When the promise was kept instead, the failure message includes
the kept value:

```
expected Promise to be broken, but it was kept with "happy"
```

When the promise was still pending after the timeout:

```
expected Promise to be broken within 0.05s, but it was still pending
```

The negated form surfaces the broken cause so you can see what
broke when you expected it not to:

```
expected Promise not to be broken, but it was (X::AdHoc: boom)
```

The matcher exposes `value` (set when the promise was kept), `cause`
(set when broken), `was-kept`, `timed-out`, and `promise-given`.

## CompleteWithinMatcher (built-in)

`complete-within` passes when the promise settles — either kept or
broken — within the given duration. The duration is a `Real`
positional argument in seconds:

```raku
expect(Promise.kept('done')).to.complete-within(1);
expect(Promise.broken('boom')).to.complete-within(1);
expect(start { sleep 0.01; 42 }).to.complete-within(1);

expect($pending).to.not.complete-within(0.05);
```

The failure message when the promise is still pending:

```
expected Promise to complete within 0.05s, but it was still pending
```

The negated form indicates the final status:

```
expected Promise not to complete within 1s, but it was kept with "done"
expected Promise not to complete within 1s, but it was broken (X::AdHoc: boom)
```

`Failure.expected` carries the duration so failure metadata is
queryable from tooling. The matcher exposes `final-status` (`Kept`,
`Broken`, or `Nil` when timed out), `value`, `cause`, `timed-out`,
and `promise-given`.

Unlike `be-kept` / `be-broken`, the duration is required — there's
no useful default for "how long is too long" without it.

## EmitMatcher (built-in)

`emit` taps a `Supply` or iterates a `Channel` and passes when the
source emits exactly the expected sequence of values within a
configurable collection window. Comparison is element-by-element via
`eqv`:

```raku
expect(Supply.from-list(1, 2, 3)).to.emit(1, 2, 3);
expect(Supply.from-list('a', 'b')).to.emit('a', 'b');

my $ch = Channel.new;
$ch.send(1); $ch.send(2); $ch.close;
expect($ch).to.emit(1, 2);

expect(Supply.from-list(1, 2)).to.not.emit(1, 2, 3);
```

The collection window defaults to 1 second. Pass `:within(seconds)` to
change it — useful for slow streams or to fail fast on hung supplies:

```raku
expect(Supply.from-list(1, 2)).to.emit(1, 2, :within(0.5));
```

`emit` uses `react`/`whenever` under the hood and stops collecting as
soon as the expected number of values arrive (the window is only the
maximum wait). This makes the matcher fast for known-length sequences.

Failure messages render both the expected and the actually-emitted
sequences:

```
expected stream to emit [1, 2, 3] within 1s, but it emitted [1, 2]
```

Non-stream actuals (anything that's neither `Supply` nor `Channel`)
record a shape-failure rather than dying:

```
expected a Supply or Channel for emit, but got 42
```

`Failure.expected` carries the expected values list for tooling.

## EmitAtLeastMatcher (built-in)

`emit-at-least` passes when a `Supply` or `Channel` emits at least the
given number of values within the collection window. The matcher stops
collecting as soon as the threshold is reached:

```raku
expect(Supply.from-list(1, 2, 3, 4)).to.emit-at-least(2);
expect(Supply.from-list('a')).to.not.emit-at-least(3, :within(0.1));
```

The `:within` window defaults to 1 second.

Failure messages report the count that was actually emitted:

```
expected stream to emit at least 3 values within 0.1s, but it emitted 1
```

`Failure.expected` carries the minimum count.

## CompleteMatcher (built-in)

`complete` waits for a `Supply` to send `done` (or for a `Channel` to
be closed) within the collection window:

```raku
expect(Supply.from-list(1, 2, 3)).to.complete;

my $ch = Channel.new;
$ch.send(1); $ch.close;
expect($ch).to.complete;

my $supplier = Supplier.new;
expect($supplier.Supply).to.not.complete(:within(0.1));
```

The `:within` window defaults to 1 second.

Failure messages report whether the stream was still active or had
errored:

```
expected stream to complete within 0.1s, but it was still active (emitted 0 values)
expected stream to complete within 1s, but it quit (X::AdHoc: boom)
```

`Failure.expected` carries the window so failure metadata is queryable
from tooling.

Unlike Promise's `complete-within`, the `complete` matcher does not
treat a stream-level error (`QUIT`) as completion — only the `done`
signal counts.

## EventuallyMatcher (built-in)

`eventually` re-runs a `Callable` block on a polling loop until the
inner matcher passes or the timeout elapses. Useful for asserting on
eventually-consistent state without hand-rolled polling:

```raku
expect({ get-job-status() }).to.eventually.be('done');
expect({ counter() }).to.eventually(:timeout(5)).be-greater-than(100);
expect({ log-buffer() }).to.eventually.match(/'ready'/);
```

The chained matcher methods (`.be`, `.eq`, `.match`, `.include`,
`.be-truthy`, `.be-a`, `.be-greater-than`, etc.) build the appropriate
inner matcher and feed each polled value through it. For matchers
outside the built-in chained set, pass a `Matcher` instance through
`.matches-with`:

```raku
expect({ load() }).to.eventually.matches-with(MyCustomMatcher.new(...));
```

The `eventually(...)` call takes two named arguments:

| Name        | Default | Meaning                                      |
| ----------- | ------- | -------------------------------------------- |
| `:timeout`  | 2.0     | Max seconds to wait for a match              |
| `:interval` | 0.05    | Seconds to sleep between iterations          |

Polling exits as soon as the inner matcher passes, so a passing
assertion completes quickly. A miss runs out the full timeout before
recording a failure.

Failure messages report the iteration count and the elapsed wall-clock
time:

```
eventually: Expected: 99 to be 0 (after 5 iterations in 0.05s)
```

If the block throws on every iteration, the failure message names the
last exception:

```
eventually: block threw X::AdHoc: not yet (after 5 iterations in 0.05s)
```

Exceptions raised inside the block are caught per-iteration and treated
as misses — the polling loop continues so the block has a chance to
recover (e.g., a service warming up).

### Negation

Under `.not`, `eventually` passes only when the inner matcher never
matched for the full timeout window:

```raku
expect({ get-status() }).to.not.eventually(:timeout(0.1)).be('error');
```

Because polling exits at the first match, a negated assertion that
matches early fails quickly rather than waiting out the timeout.

### Failure metadata

- `Failure.given` is the `Callable` block (preserved as-is)
- `Failure.expected` is the inner matcher's `expected-value`
- `Failure.message` includes iteration count and elapsed time

### Non-Callable actuals

`expect(42).to.eventually.be(42)` records a Promise-shape failure
because the actual is not a `Callable`. The block form is required.

## Writing a custom matcher

Define a class that does `Matcher` and pass an instance to `.be(...)`:

```raku
use BDD::Behave;
use BDD::Behave::Matcher;

class EvenMatcher does Matcher {
  method matches($actual --> Bool) { ?($actual %% 2) }
  method failure-message($actual --> Str) {
    "expected $actual to be even";
  }
  method failure-message-negated($actual --> Str) {
    "expected $actual not to be even";
  }
  method expected-value(--> Mu) { 'an even number' }
}

it 'is even', {
  expect(4).to.be(EvenMatcher.new);          # passes
  expect(5).to.not.be(EvenMatcher.new);      # passes
}
```

When the matcher reports a failure:

- The matcher's `failure-message($actual)` (or `failure-message-negated($actual)`
  for `.not`) becomes `Failure.message` and is what the failure summary prints.
- `Failure.given` holds the actual value, `Failure.expected` holds
  `expected-value`. Both are still useful for programmatic consumers and for
  alternate formatters.

## Negation

`.not` flips the boolean result of `matches` *before* the framework decides
whether to record a failure. Matchers do not need to special-case negation;
they only need to return a sensible message from `failure-message-negated` for
when `.not` fails (i.e., the matcher matched but should not have).

Built-in matchers and user-supplied matchers go through the same role, so a
custom matcher you write integrates with `expect` exactly the way the
built-ins do.

For the DSL-form alternative — defining matchers as a bundle of callbacks
without writing a class — see [Custom Matchers](custom-matchers.md).

## Composing matchers

Every type that does `Matcher` automatically gets `.and` and `.or` methods
that build `AndMatcher` / `OrMatcher` composites. They short-circuit, flatten
across chained calls, and report which inner matcher decided the outcome. See
[Composable Matchers](composable-matchers.md) for details.
