PIN (Piece Identifier Notation) implementation for Rust.
This crate implements the PIN Specification v1.0.0.
PIN is a compact, ASCII-only token format that encodes a piece identity at the
level of notation: the tuple (piece name, side, state, terminal status). The case
of a single letter encodes the side, an optional +/- prefix encodes the state,
and an optional ^ suffix marks a terminal piece.
[<state-modifier>]<abbr>[<terminal-marker>] e.g. K +R k^ -K^ +G^
PIN standardizes only the encoding. What a state means, which pieces are terminal, and how players are named are all left to the rule system — see the Game Protocol and Glossary.
| Property | Value | Rationale |
|---|---|---|
| Token length | 1–3 bytes | ^[+-]?[A-Za-z]\^?$ per the specification |
| Closed domain | 312 tokens | 26 letters × 2 sides × 3 states × 2 terminal flags |
Identifier size |
4 bytes, Copy |
stored inline; parsing and encoding never allocate |
| Dependencies | none required | zero by default; serde is an optional, no_std add-on |
unsafe |
forbidden | the crate is built under a forbid-unsafe lint policy |
| MSRV | 1.81 | for core::error::Error without a std feature |
cargo add sashite-pinOr add it manually to Cargo.toml:
[dependencies]
sashite-pin = "1"serde(off by default) — implementsSerialize/DeserializeforIdentifier, (de)serializing it as its canonical token string (e.g."+K^"). Enabling it keeps the crateno_std.
[dependencies]
sashite-pin = { version = "1", features = ["serde"] }use sashite_pin::{Identifier, Side, State};
let king: Identifier = "+K^".parse()?; // via FromStr
let rook = Identifier::parse("r")?; // via the inherent method
assert_eq!(king.letter().as_char(), 'K');
assert_eq!(king.side(), Side::First);
assert_eq!(king.state(), State::Enhanced);
assert!(king.is_terminal());
assert_eq!(rook.side(), Side::Second);Construction is infallible: because each component type is valid by construction, every combination denotes a valid token.
use sashite_pin::{Identifier, Letter, Side, State};
let pawn = Identifier::new(
Letter::try_from_char('P')?,
Side::Second,
State::Enhanced,
false,
);
assert_eq!(pawn.encode().as_str(), "+p");encode returns an allocation-free, fixed-buffer string view that dereferences
to str; Display writes the same canonical form.
use sashite_pin::Identifier;
let id = Identifier::parse("+K^")?;
assert_eq!(id.encode().as_str(), "+K^");
assert_eq!(id.to_string(), "+K^"); // requires `alloc`/`std`use sashite_pin::Identifier;
assert!(Identifier::is_valid("+K^"));
assert!(!Identifier::is_valid("K+")); // a modifier must be a prefixEvery transformation returns a new value (the type is Copy, so this is cheap):
use sashite_pin::Identifier;
let white = Identifier::parse("+P")?;
assert_eq!(white.flipped().encode().as_str(), "+p");
assert_eq!(white.normalized().encode().as_str(), "P");
assert_eq!(white.diminished().with_terminal(true).encode().as_str(), "-P^");
assert!(white.is_first());
assert!(white.is_enhanced());The grammar (EBNF) is:
pin ::= [ state-modifier ] abbr [ terminal-marker ] ;
state-modifier ::= "+" | "-" ;
abbr ::= "A"…"Z" | "a"…"z" ;
terminal-marker ::= "^" ;A token maps to exactly four attributes:
| 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 |
Letters are not reserved: the mapping from abbreviation to full piece name is defined entirely by the rule system. See the examples page for sample mappings (chess, shogi, xiangqi, makruk).
no_stdand allocation-free. Parsing borrows the input bytes; anIdentifieris a 4-byteCopyvalue andEncodedPinkeeps the ≤ 3 output bytes in a fixed inline buffer. Nothing touches the heap.- No
unsafe, no regex engine. The parser matches raw bytes directly, eliminating ReDoS as an attack vector. - Bounded, panic-free parsing. Inputs longer than three bytes are rejected on
a structural length check before any byte is inspected, and the public parsing
API returns a
Resultrather than panicking. const-friendly. Construction, parsing, validation, the accessors, and the transformations are allconst fn, so identifiers can be built and checked at compile time.- Total component construction. With valid-by-construction component types, building an identifier from its parts cannot fail.
The hot paths run in single-digit nanoseconds per call. Indicative figures from
benches/parse.rs:
| Operation | Time |
|---|---|
parse +K^ (3 bytes) |
~3.2 ns |
parse K (1 byte) |
~3.6 ns |
| reject over-long input | ~1.8 ns |
Run them with cargo bench.
- Game Protocol — the conceptual foundation
- PIN Specification v1.0.0 — the normative document
- PIN Examples — sample piece-set mappings
Reference implementations in other languages are maintained by Sashité: Elixir, Go, 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.