Skip to main content

sashite_feen/
lib.rs

1//! # Field Expression Encoding Notation (FEEN)
2//!
3//! A `no_std`, `unsafe`-free, allocation-free implementation of the
4//! [FEEN v1.0.0 specification](https://sashite.dev/specs/feen/1.0.0/), built on
5//! [EPIN](https://sashite.dev/specs/epin/1.0.0/) (piece tokens) and
6//! [SIN](https://sashite.dev/specs/sin/1.0.0/) (style tokens).
7//!
8//! FEEN encodes a complete board-game **position** as three space-separated
9//! fields:
10//!
11//! ```text
12//! <piece-placement> <hands> <style-turn>
13//! ```
14//!
15//! for example the chess starting position
16//! `-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 / C/c`.
17//!
18//! ## Allocation-free by design
19//!
20//! Unlike a fixed-size token, a position is variable-sized, so this crate is
21//! built as a **borrowing, streaming validator** rather than a parser that
22//! returns an owned value:
23//!
24//! - [`Feen::parse`] validates an input string in a single pass and returns a
25//!   [`Feen`] *view* that borrows it — nothing is materialized, nothing is
26//!   allocated.
27//! - The view exposes the geometry ([`Feen::shape`]), the square and piece
28//!   counts, the active player, and **lazy iterators** over board squares and
29//!   hand items.
30//! - An owned position is available behind the `alloc` feature as a
31//!   [`sashite_qi::Qi`], produced by [`Feen::to_qi`]; [`encode`] turns one back
32//!   into a canonical FEEN string. That conversion is the only part of the crate
33//!   that allocates.
34//!
35//! Going the other way is fallible: `Qi` is a more permissive model than FEEN,
36//! so [`encode`] returns an [`EncodeError`] for a position FEEN cannot spell
37//! rather than a string [`Feen::parse`] would reject. Whatever it returns as
38//! `Ok`, this crate parses back into the position it started from.
39//!
40//! The default build does not even link the `alloc` crate.
41//!
42//! ## Bounds (assumed for safety)
43//!
44//! Inputs are bounded before and during parsing, which keeps memory and time
45//! bounded even on untrusted input:
46//!
47//! - at most [`MAX_STRING_LENGTH`] bytes (checked before any parsing),
48//! - at most [`MAX_DIMENSIONS`] board dimensions,
49//! - at most [`MAX_DIMENSION_SIZE`] cells along any dimension,
50//! - at most [`MAX_SQUARE_COUNT`] squares in total,
51//! - **regular shapes only**: every rank within a dimension has the same length.
52//!
53//! **All five make this crate intentionally *stricter* than the FEEN
54//! specification**, which places no ceiling on dimensionality, dimension size or
55//! square count, and which also permits irregular boards. The corresponding
56//! rejections are [`ParseError::InputTooLong`],
57//! [`ParseError::TooManyDimensions`], [`ParseError::DimensionTooLarge`],
58//! [`ParseError::TooManySquares`] and [`ParseError::BoardNotRegular`]. Each
59//! bound sits far beyond any real board, and none of them changes how a string
60//! the crate *does* accept is interpreted.
61//!
62//! The byte cap and the square cap bound different things, and which one binds
63//! depends on how full the board is: an empty 255 × 255 board spells out in
64//! 1 019 bytes and is accepted, while a fully occupied one would need 65 279 and
65//! is not. See [`MAX_STRING_LENGTH`] for the figures.
66//!
67//! ## Example
68//!
69//! ```
70//! # fn main() -> Result<(), sashite_feen::ParseError> {
71//! use sashite_feen::{Feen, Side};
72//!
73//! let feen = Feen::parse("8/8/8/8/8/8/8/8 / C/c")?;
74//! assert_eq!(feen.square_count(), 64);
75//! assert_eq!(feen.piece_count(), 0);
76//! assert_eq!(feen.active_side(), Side::First);
77//!
78//! assert!(Feen::is_valid("k^+p4+PK^ / C/c")); // a 1D, 8-square board
79//! # Ok(())
80//! # }
81//! ```
82//!
83//! ## Guarantees
84//!
85//! - **`no_std` and allocation-free core.** Validation and iteration borrow the
86//!   input; nothing touches the heap unless the `alloc` feature is used.
87//! - **No `unsafe`, no regex engine.** Parsing is a single left-to-right pass
88//!   over raw bytes, with bounded integer arithmetic.
89//! - **Built on EPIN and SIN.** Piece and style tokens are validated by those
90//!   crates; FEEN adds only the field, dimensional, canonicality, and
91//!   cardinality rules.
92
93// `no_std` for every normal build; under `cargo test` the crate links `std` so
94// the unit-test harness (and the in-module unit tests) can run. Consumers and
95// the `no_std` CI job (which builds `--lib`, not tests) still get `no_std`.
96#![cfg_attr(not(test), no_std)]
97
98#[cfg(feature = "alloc")]
99extern crate alloc;
100
101mod encode;
102mod error;
103mod feen;
104mod hands;
105mod limits;
106mod parse;
107mod placement;
108#[cfg(feature = "serde")]
109mod serde_impl;
110mod shape;
111mod style_turn;
112mod token;
113
114/// Re-export of the [`sashite_epin`] crate, which provides the piece-token type
115/// surfaced by this crate's board and hand iterators.
116pub use sashite_epin;
117/// Re-export of the [`sashite_sin`] crate, which provides the style-token type
118/// surfaced by the style–turn accessors.
119pub use sashite_sin;
120
121/// Re-export of the [`sashite_qi`] crate, whose [`Qi`](sashite_qi::Qi) is the
122/// owned position type produced by [`Feen::to_qi`]. Available with the `alloc`
123/// feature.
124#[cfg(feature = "alloc")]
125pub use sashite_qi;
126
127/// The two-side model (`First` / `second`) shared across the ecosystem,
128/// re-exported from [`sashite_sin`]. Used here for the active player and the
129/// hand attribution.
130pub use sashite_sin::Side;
131
132pub use crate::error::ParseError;
133pub use crate::feen::{Feen, SquareIter};
134pub use crate::hands::{HandItem, HandIter};
135pub use crate::limits::{MAX_DIMENSIONS, MAX_DIMENSION_SIZE, MAX_SQUARE_COUNT, MAX_STRING_LENGTH};
136pub use crate::shape::Shape;
137
138/// Canonical FEEN serialization of an owned [`Qi`](sashite_qi::Qi) position,
139/// and the reasons it can fail. Available with the `alloc` feature.
140#[cfg(feature = "alloc")]
141pub use crate::encode::{encode, write_feen};
142#[cfg(feature = "alloc")]
143pub use crate::error::EncodeError;
144
145/// A `#[serde(with = "…")]` adapter that (de)serializes a
146/// [`Qi`](sashite_qi::Qi) position as its canonical FEEN string. Available with
147/// the `serde` feature.
148#[cfg(feature = "serde")]
149pub use crate::serde_impl::feen_string;
150
151/// Compiles and runs the README's examples as doctests.
152///
153/// The README is the crate's front page on crates.io and the first code a
154/// reader copies, so it is held to the same standard as the rustdoc above: if
155/// an example there stops compiling, `cargo test` fails. `include_str!` also
156/// means the file cannot drift out of the published tarball's reach — it is the
157/// same bytes in both places.
158#[cfg(doctest)]
159#[doc = include_str!("../README.md")]
160struct ReadmeDoctests;