EPIN (Extended Piece Identifier Notation) implementation for Rust.
This crate implements the EPIN Specification v1.0.0.
EPIN is a strict superset of PIN: it inherits
the four PIN attributes (piece name, side, state, terminal status) and adds a single
optional trailing marker, the derivation marker ', which flags whether a piece's
style is native or derived.
<pin>['] e.g. K r' +K^ -k^'
EPIN encodes only the flag. What "native" and "derived" mean, and how a concrete style is resolved, is left to the surrounding context — see the Game Protocol and Glossary.
This crate is a thin layer over sashite-pin:
an Identifier is a PIN identifier paired with the native/derived flag, and all
PIN-level parsing, validation, and encoding is delegated to that crate.
| Property | Value | Rationale |
|---|---|---|
| Token length | 1–4 bytes | \A[-+]?[A-Za-z]\^?'?\z per the specification |
| Closed domain | 624 tokens | the 312 PIN tokens × 2 style-status flags |
Identifier size |
5 bytes, Copy |
a PIN identifier (4 bytes) plus the style-status flag |
| Dependencies | sashite-pin only |
optional serde; no other runtime dependencies |
unsafe |
forbidden | the crate is built under a forbid-unsafe lint policy |
| MSRV | 1.81 | inherited from sashite-pin (core::error::Error) |
cargo add sashite-epinOr add it manually to Cargo.toml:
[dependencies]
sashite-epin = "1"serde(off by default) — implementsSerialize/DeserializeforIdentifier, (de)serializing it as its canonical token string (e.g."+K^'"). It also turns onsashite-pin's matching feature, and keeps the crateno_std.
[dependencies]
sashite-epin = { version = "1", features = ["serde"] }use sashite_epin::{Identifier, Side, State};
let king: Identifier = "+K^'".parse().expect("a valid EPIN token"); // via FromStr
let rook = Identifier::parse("r'").expect("a valid EPIN token"); // inherent
assert_eq!(king.letter().as_char(), 'K');
assert_eq!(king.side(), Side::First);
assert_eq!(king.state(), State::Enhanced);
assert!(king.is_terminal());
assert!(king.is_derived());
assert!(rook.is_second());
assert!(rook.is_derived());pin returns the underlying PIN identifier — the escape hatch to the full PIN API
(queries and transformations not re-exposed directly on the EPIN type).
use sashite_epin::Identifier;
let king = Identifier::parse("+K^'").expect("a valid EPIN token");
assert_eq!(king.pin().encode().as_str(), "+K^");
assert!(king.pin().is_enhanced());native and derive flip only the style-status flag and are idempotent;
with_pin swaps the PIN core while preserving that flag.
use sashite_epin::Identifier;
let king = Identifier::parse("+K^'").expect("a valid EPIN token");
assert_eq!(king.native().encode().as_str(), "+K^");
assert_eq!(king.derive().encode().as_str(), "+K^'");
// Compose with PIN's own transformations through `with_pin`.
let flipped = king.with_pin(king.pin().flipped());
assert_eq!(flipped.encode().as_str(), "+k^'");Construction is infallible: every PIN identifier is a valid EPIN core.
use sashite_epin::Identifier;
use sashite_pin::Identifier as Pin;
let derived = Identifier::new(Pin::parse("+r").expect("a valid PIN token"), true);
assert_eq!(derived.encode().as_str(), "+r'");use sashite_epin::Identifier;
assert!(Identifier::is_valid("r'"));
assert!(!Identifier::is_valid("K'^")); // the marker must be lastThe grammar (EBNF) is:
epin ::= pin [ derivation-marker ] ;
derivation-marker ::= "'" ;For the pin production, see the PIN specification.
A token maps to the four PIN attributes plus one flag:
| Component | Encodes | Values |
|---|---|---|
| letter case | side | uppercase → First, lowercase → Second |
| letter | piece name | a single-letter abbreviation (A–Z) |
+ / - prefix |
state | Enhanced / Diminished (else Normal) |
^ suffix |
terminal status | present → terminal piece |
' suffix |
style status | present → derived (else native) |
- No
'→ style status is native - With
'→ style status is derived
That is the only universal meaning EPIN assigns to '. Resolving a concrete style
from this flag is context-defined.
EPIN re-exports the PIN layer, so downstream code needs no separate sashite-pin
dependency to name the underlying types:
use sashite_epin::{Letter, Side, State}; // re-exported from sashite-pin
use sashite_epin::sashite_pin::Identifier as Pin; // the PIN identifier itselfBecause sashite_pin::Identifier appears in EPIN's public API (as the return type of
Identifier::pin), the "1" version requirement relies on PIN's 1.x semver stability.
no_stdand allocation-free. AnIdentifierborrows nothing — it is a self-contained 5-byteCopyvalue — andEncodedEpinkeeps the ≤ 4 output bytes in a fixed inline buffer. Nothing touches the heap, and no output outlives its input.- Panic-free. The library contains no indexing, no fallible arithmetic and no
panicking call: a clippy census with
indexing_slicing,arithmetic_side_effects,panic,unwrap_usedandexpect_usedenabled reports zero sites. That matters in a crate meant for bare-metal targets, where a panic is not a nice failure mode. - String-like formatting.
Identifier,EncodedEpinandParseErrorall render throughcore::fmt::Formatter::pad, so width, fill, alignment and precision work as they do forstr. - Comparable encodings.
EncodedEpinimplementsPartialEq,Eq,Hash,PartialOrdandOrd, as well as comparison againststr. Its order is the token text's, which is deliberately not the attribute orderIdentifierderives —-K < +Kby identifier,+K < -Kby text. - No
unsafe, no regex engine. Parsing detaches an optional trailing'and hands the core tosashite-pin, which matches raw bytes directly. - Single source of truth. All PIN-level parsing, validation, and encoding lives
in
sashite-pin; EPIN adds only the'marker, so the two can never diverge. const-friendly construction.new, the accessors, and thenative/derive/with_pintransforms areconst fn.parse,is_validandencodeare not: they compose PIN's string/byte API, and the pieces they need are not yetconstat the 1.81 MSRV. The crate docs spell out which blocker applies to which.- Error chaining. A failure in the PIN core is wrapped in
ParseError::Pin, whosesource()returns the underlyingsashite_pin::ParseError.
The hot paths are a thin layer over sashite-pin's (which run in single-digit
nanoseconds): parsing adds one suffix check, and encoding appends at most one byte.
Run cargo bench for figures on your machine; see benches/parse.rs.
- Game Protocol — the conceptual foundation
- PIN Specification v1.0.0 — the core EPIN builds on
- EPIN Specification v1.0.0 — the normative document
- EPIN Examples — context-driven modeling patterns
Reference implementations in other languages are maintained by Sashité: Elixir, Ruby.
If a library's behavior appears to conflict with the specification, the specification is normative.
Available as open source under the terms of the Apache License 2.0.