sashite-feen 0.1.0

Field Expression Encoding Notation (FEEN): a compact, ASCII-only, no_std, zero-allocation validator and encoder for board-game positions in abstract strategy games, built on EPIN and SIN.
Documentation
# sashite-feen

[![Crates.io](https://img.shields.io/crates/v/sashite-feen.svg)](https://crates.io/crates/sashite-feen)
[![Docs.rs](https://docs.rs/sashite-feen/badge.svg)](https://docs.rs/sashite-feen)
[![CI](https://github.com/sashite/feen.rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sashite/feen.rs/actions/workflows/ci.yml)
[![License](https://img.shields.io/crates/l/sashite-feen.svg)](https://github.com/sashite/feen.rs/blob/main/LICENSE)

> A `no_std`, zero-allocation validator and encoder for **FEEN** board-game positions.

## Overview

**Field Expression Encoding Notation (FEEN)** is a compact, ASCII-only string
that encodes a complete board-game **position** as three space-separated fields:

```text
<piece-placement> <hands> <style-turn>
```

| Field           | Encodes                                                          | Example          |
|-----------------|-----------------------------------------------------------------|------------------|
| Piece placement | Board geometry and occupancy (runs of empties, dimensional `/`) | `8/8/…/RNBQK^BNR` |
| Hands           | Off-board pieces held by each player, with multiplicities       | `3P2B/3p2b`      |
| Style–turn      | One style per side, and which side is to move                   | `W/w`            |

This crate implements the
[FEEN v1.0.0 specification](https://sashite.dev/specs/feen/1.0.0/). It delegates
piece-token syntax to [EPIN](https://sashite.dev/specs/epin/1.0.0/) and
style-token syntax to [SIN](https://sashite.dev/specs/sin/1.0.0/), adding the
field, dimensional, canonicality, and cardinality rules on top.

A FEEN string is variable-sized, so — unlike a fixed-width token — this crate is
built as a **borrowing, streaming validator** rather than a parser that returns
an owned tree: [`Feen::parse`] validates the input in a single pass and hands
back a *view* that borrows it. Nothing is materialized and nothing is allocated.
An owned, transformable position is available on demand behind the `alloc`
feature as a [`Qi`](https://crates.io/crates/sashite-qi).

## Quick Start

```rust
use sashite_feen::{Feen, Side};

// Validate and parse in one pass; the view borrows the input string.
let feen =
    Feen::parse("-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / W/w")?;

assert_eq!(feen.square_count(), 64);
assert_eq!(feen.piece_count(), 32);
assert_eq!(feen.active_side(), Side::First); // active token `W` is uppercase

// A cheap boolean check when you don't need the view.
assert!(Feen::is_valid("k^+p4+PK^ / W/w")); // a 1-D, 8-square board
# Ok::<(), sashite_feen::ParseError>(())
```

## Installation

```sh
cargo add sashite-feen
```

Or add it to `Cargo.toml`:

```toml
[dependencies]
sashite-feen = "0.1"
```

`sashite-feen` is `no_std`, forbids `unsafe`, and uses no regex engine. Its only
required dependencies are [`sashite-epin`](https://crates.io/crates/sashite-epin)
and [`sashite-sin`](https://crates.io/crates/sashite-sin) (both `no_std` and
allocation-free). The minimum supported Rust version is **1.81**.

## Cargo features

The default build is **strictly allocation-free**: the `alloc` crate is not even
linked, so validation, borrowing iteration, and encoding-to-a-sink work on
targets without an allocator.

- **`alloc`** *(off by default)* — enables the owned position type
  [`sashite_qi::Qi`] and the conversions that produce or consume it:
  [`Feen::to_qi`], [`encode`], and [`write_feen`]. This is the only part of the
  crate that allocates.
- **`serde`** *(off by default)* — provides [`feen_string`], a
  `#[serde(with = "…")]` adapter that (de)serializes a `Qi` position as its
  canonical FEEN string. Implies `alloc`.

```toml
[dependencies]
sashite-feen = { version = "0.1", features = ["serde"] }
```

## Usage

### Validating and parsing

[`Feen::is_valid`] returns a `bool`; [`Feen::parse`] returns a borrowing view or
a [`ParseError`] explaining the first violation.

```rust
use sashite_feen::Feen;

assert!(Feen::is_valid("8/8/8/8/8/8/8/8 / W/w"));
assert!(Feen::parse("8/8/8/8/8/8/8/8 / W/w").is_ok());
assert!(Feen::parse("8/8/8/8/8/8/8/8 W/w").is_err()); // missing a field
```

### Reading a position

All accessors are `const fn` and borrow nothing beyond `self`. Geometry comes
from [`Shape`]; sides and styles are reported both by turn (active / inactive)
and by side (first / second).

```rust
use sashite_feen::{Feen, Side};

let feen =
    Feen::parse("lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / J/j")?;

let shape = feen.shape();
assert_eq!(shape.dimension_count(), 2);
assert_eq!(shape.square_count(), 81);          // also `feen.square_count()`
assert_eq!(shape.dimensions(), &[9u8, 9]);     // sizes along each dimension

assert_eq!(feen.piece_count(), 40);            // on-board + in-hand
assert_eq!(feen.board_piece_count(), 40);
assert_eq!(feen.hand_piece_count(), 0);

assert_eq!(feen.active_side(), Side::First);   // `J` is uppercase ⇒ first
assert_eq!(feen.inactive_side(), Side::Second);
# Ok::<(), sashite_feen::ParseError>(())
```

### Iterating squares and hands

The board iterator yields one `Option<Piece>` per square in the board's
serialization order (`None` for an empty square); the hand iterators yield
[`HandItem`]s in canonical order. Both are lazy and allocation-free; `Piece` is
an [`sashite_epin::Identifier`].

```rust
use sashite_feen::Feen;

let feen = Feen::parse("4P3/8 P/p W/w")?; // 2 ranks; first hand: one P, second: one p

let occupied = feen.squares().flatten().count();
assert_eq!(occupied, 1); // the lone `P` on the board

for item in feen.first_hand() {
    // `item.piece()` is an EPIN identifier; `item.count()` ≥ 1.
    assert_eq!(item.count(), 1);
}
# Ok::<(), sashite_feen::ParseError>(())
```

### Owned positions (`alloc`)

With the `alloc` feature, [`Feen::to_qi`] materializes the view into a
[`Qi`](sashite_qi::Qi) — the ecosystem's owned, immutable position type — and
[`encode`] / [`write_feen`] turn an owned position back into a canonical FEEN
string. Re-encoding an unchanged position reproduces the input exactly.

```rust,ignore
use sashite_feen::{encode, Feen};

let feen = Feen::parse("8/8/8/8/8/8/8/8 / W/w")?;
let position = feen.to_qi();           // allocates the owned position

assert_eq!(encode(&position), "8/8/8/8/8/8/8/8 / W/w");
```

Because `Qi` is generic and transformable, you can read a position with FEEN,
edit it with `Qi`'s move-based API, and re-encode it — see `examples/basic.rs`
(`cargo run --example basic --features alloc`).

### Serde (`serde`)

[`feen_string`] lets a `Qi` field round-trip through its canonical FEEN string,
which keeps the serialized form human-readable and portable:

```rust,ignore
use serde::{Deserialize, Serialize};
use sashite_feen::sashite_qi::Qi;
use sashite_feen::sashite_epin::Identifier as Piece;
use sashite_feen::sashite_sin::Identifier as Style;

#[derive(Serialize, Deserialize)]
struct Saved {
    #[serde(with = "sashite_feen::feen_string")]
    position: Qi<Piece, Style>,
}
```

## The FEEN string

### Piece placement

A run of consecutive empty squares is written as a base-10 count (`≥ 1`, no
leading zeros); every other run is a sequence of EPIN piece tokens. Dimensions
are separated hierarchically: a single `/` separates ranks, `//` separates
2-D layers, `///` separates 3-D cubes, and so on. **Dimensional coherence**
requires that a separator of length `N` only appear between structures that
themselves contain separators of length `N − 1`.

```text
rkr                         # 1-D, 3 squares
8/8/8/8/8/8/8/8             # 2-D, 8×8 = 64 empty squares
ab/cd//AB/CD               # 3-D, 2 layers × 2 ranks × 2 files
```

> **Stricter than the specification:** this crate accepts **regular boards
> only** — every rank within a dimension must have the same length. Inputs the
> specification would allow as irregular are rejected with
> [`ParseError::BoardNotRegular`].

### Hands

`<first-hand>/<second-hand>`, each a separator-free concatenation of items
`[<count>]<piece>` in canonical order. The count is omitted when it is 1 and
must be `≥ 2` when present. The piece's own side (its token's case) is
independent of which hand holds it.

### Style–turn

`<active-style>/<inactive-style>`, each a single SIN letter. **Case** encodes
the player side (uppercase ⇒ `first`, lowercase ⇒ `second`); **position**
encodes the turn (the first token is the active player). The two tokens must be
of opposite case.

## Bounds (assumed for safety)

Inputs are bounded before and during parsing, so memory and time stay bounded
even on untrusted input:

| Constant              | Meaning                                          |
|-----------------------|--------------------------------------------------|
| [`MAX_STRING_LENGTH`] | Maximum input length in bytes (checked first)    |
| [`MAX_DIMENSIONS`]    | Maximum number of board dimensions               |
| [`MAX_DIMENSION_SIZE`]| Maximum number of cells along any one dimension  |

A separate internal cap on the total square count (rejected with
[`ParseError::TooManySquares`]) guarantees that any FEEN string this crate
accepts is constructible as a [`Qi`](sashite_qi::Qi) without overflow.

## Errors

[`ParseError`] reports the first violation found. The variants group by field:

| Group        | Variants (selected)                                                                                             |
|--------------|-----------------------------------------------------------------------------------------------------------------|
| Lexical      | `InputTooLong`, `NonAscii`, `FieldCount`                                                                         |
| Placement    | `PlacementEmpty`, `PlacementStartsWithSeparator`, `PlacementEndsWithSeparator`, `EmptySegment`, `InvalidEmptyCount`, `InvalidPieceToken`, `BoardNotRegular`, `DimensionalCoherence`, `TooManyDimensions`, `DimensionTooLarge` |
| Hands        | `InvalidHandsDelimiter`, `InvalidHandCount`, `HandNotAggregated`, `HandNotCanonical`                            |
| Style–turn   | `InvalidStyleTurnDelimiter`, `InvalidStyleToken`, `StylesSameCase`                                              |
| Cardinality  | `TooManySquares`, `TooManyPieces`                                                                               |

## Design

- **Allocation-free, borrowing core.** Validation is one left-to-right pass over
  the raw bytes; the view and its iterators borrow the input. The heap is touched
  only through the optional `alloc`-gated conversions.
- **No `unsafe`, no regex engine.** Parsing matches bytes directly with bounded
  integer arithmetic, eliminating ReDoS as an attack vector.
- **Layered on EPIN and SIN.** Piece and style tokens are validated by those
  crates; this crate owns only the FEEN-level structure.
- **Canonical-only.** Non-minimal empty counts and non-canonical hand orderings
  are rejected, so an accepted string is already in canonical form.

## Ecosystem

FEEN is the position-serialization layer of the
[Sashité](https://sashite.dev/) ecosystem. It describes *how a position is
written*, while the surrounding crates supply the pieces, the styles, and the
owned model:

- [Qi]https://crates.io/crates/sashite-qi — the owned, immutable position type
- [EPIN]https://sashite.dev/specs/epin/1.0.0/ — piece token syntax
- [SIN]https://sashite.dev/specs/sin/1.0.0/ — style token syntax
- [Game Protocol]https://sashite.dev/game-protocol/ — the shared conceptual foundation

If a behavior here appears to conflict with the
[specification](https://sashite.dev/specs/feen/1.0.0/), the specification is
normative.

## License

Available as open source under the terms of the
[Apache License 2.0](https://opensource.org/licenses/Apache-2.0).