# Labels

Labels are sentinel values for tagging and filtering tests.

## Defining labels

Use `#[skuld::label]` on a constant declaration. The label's string name is the identifier lowercased — `DOCKER` becomes the name `"docker"`.

```rust
#[skuld::label] pub const DOCKER: skuld::Label;
#[skuld::label] pub const SLOW: skuld::Label;
#[skuld::label] pub const UNIT: skuld::Label;
```

The const name must be a valid Rust identifier (ASCII letters, digits, and underscores; must not start with a digit) — it's a plain Rust `const`, so the compiler enforces the usual identifier rules on the symbol. The runtime label string is the identifier lowercased.

### Cross-crate sharing

To use a label defined in another crate, just `use` it:

```rust
use other_crate::DOCKER;

#[skuld::test(labels = [DOCKER])]
fn my_test() { /* ... */ }
```

At startup, skuld panics (with both source locations) if two `#[skuld::label]` declarations in the binary produce the same lowercased name.

## Labeling tests

Pass label constants to the `labels` option:

```rust
#[skuld::test(labels = [DOCKER, SLOW])]
fn heavy_test() { /* ... */ }

#[skuld::test(labels = [UNIT])]
fn fast_test() { /* ... */ }
```

## Filtering with `SKULD_LABELS`

Set the `SKULD_LABELS` environment variable to a boolean expression to filter tests at collection time. Tests not matching the filter do not appear at all (not ignored — absent):

```bash
SKULD_LABELS=docker cargo test                         # only tests labeled "docker"
SKULD_LABELS="docker | slow" cargo test                # tests labeled "docker" OR "slow"
SKULD_LABELS="docker & slow" cargo test                # tests labeled "docker" AND "slow"
SKULD_LABELS="!slow" cargo test                        # all tests except "slow"
SKULD_LABELS="(docker | integration) & !slow" cargo test  # combined
```

Label names in `SKULD_LABELS` are matched case-insensitively. `SKULD_LABELS=DOCKER`, `SKULD_LABELS=Docker`, and `SKULD_LABELS=docker` are equivalent.

### Expression syntax

| Operator | Meaning    | Example                           |
| -------- | ---------- | --------------------------------- |
| (none)   | bare label | `docker`                          |
| `true`   | constant   | `true` (matches every test)       |
| `false`  | constant   | `false` (matches no test)         |
| `!`      | NOT        | `!slow`                           |
| `&`      | AND        | `docker & slow`                   |
| `\|`     | OR         | `docker \| slow`                  |
| `()`     | grouping   | `(docker \| integration) & !slow` |

**Precedence** (highest to lowest): `!` > `&` > `|`

Whitespace between tokens is optional. Quote the value in shell when using `|`.

**Unset** `SKULD_LABELS` → no filtering, all tests run.

**Empty or whitespace-only** `SKULD_LABELS` (e.g. `SKULD_LABELS=""` or `SKULD_LABELS="   "`) → panic at startup with `skuld: SKULD_LABELS: ...`. Shell expansions like `SKULD_LABELS="$MAYBE_UNSET"` that produce an empty string will therefore panic, not be treated as "no filter." If you want a conditional filter, use `if [ -n "$VAR" ]; then ... fi` or similar.

The names `true` and `false` are reserved by the grammar. Attempting to define a label with one of those names (`#[skuld::label] pub const TRUE: skuld::Label`) is a compile-time error.

### Canonical form

Filters are stored in a canonical (BDD-simplified, sort-normalized) form. Two semantically-equivalent expressions are interchangeable:

- `LabelFilter::parse("a & b") == LabelFilter::parse("b & a")` — operator order does not matter.
- `LabelFilter::parse("!!a") == LabelFilter::parse("a")` — double negation collapses.
- `LabelFilter::parse("a | !a") == LabelFilter::parse("true")` — tautologies fold to `true`.
- Merged fixture filters dedup automatically — `(a) | (a) | (b)` displays as `a | b`.

Display output reflects the canonical form, so `parse(filter.to_string())` round-trips. Note that the canonical form uses sum-of-products with sorted children, so `(a | b) & c` displays as `a & c | b & c`.

## Module-level defaults

Use `default_labels!` to set default labels for all `#[skuld::test]` functions in a module:

```rust
#[skuld::label] pub const SMOKE: skuld::Label;
#[skuld::label] pub const UNIT: skuld::Label;
#[skuld::label] pub const SLOW: skuld::Label;
skuld::default_labels!(SMOKE, UNIT);

#[skuld::test]                      // inherits [SMOKE, UNIT]
fn test_a() { /* ... */ }

#[skuld::test(labels = [SLOW])]     // gets [SLOW], NOT [SMOKE, UNIT, SLOW]
fn test_b() { /* ... */ }

#[skuld::test(labels = [])]         // gets nothing (explicit opt-out)
fn test_c() { /* ... */ }
```

Explicit `labels = [...]` (including empty) **fully replaces** the module defaults — there is no merging.

Default labels are matched by module path prefix, so a `default_labels!` in a parent module applies to all children unless overridden.
