sashite-pin 1.0.0

Piece Identifier Notation (PIN): a compact, ASCII-only, no_std token format for identifying pieces in abstract strategy board games.
docs.rs failed to build sashite-pin-1.0.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: sashite-pin-1.0.1

sashite-pin

Crates.io Docs.rs CI License

PIN (Piece Identifier Notation) implementation for Rust.

Overview

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.

Implementation constraints

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

Installation

cargo add sashite-pin

Or add it manually to Cargo.toml:

[dependencies]
sashite-pin = "1"

Cargo features

  • serde (off by default) — implements Serialize / Deserialize for Identifier, (de)serializing it as its canonical token string (e.g. "+K^"). Enabling it keeps the crate no_std.
[dependencies]
sashite-pin = { version = "1", features = ["serde"] }

Usage

Parsing

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);

Building from typed components

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");

Encoding and formatting

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`

Validation

use sashite_pin::Identifier;

assert!(Identifier::is_valid("+K^"));
assert!(!Identifier::is_valid("K+"));         // a modifier must be a prefix

Transformations and queries

Every 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());

Token format

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 (AZ)
+ / - 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).

Design and guarantees

  • no_std and allocation-free. Parsing borrows the input bytes; an Identifier is a 4-byte Copy value and EncodedPin keeps 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 Result rather than panicking.
  • const-friendly. Construction, parsing, validation, the accessors, and the transformations are all const 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.

Performance

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.

Related specifications

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.

License

Available as open source under the terms of the Apache License 2.0.