README.md

# pin.ex

[![Hex.pm](https://img.shields.io/hexpm/v/sashite_pin.svg)](https://hex.pm/packages/sashite_pin)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/sashite_pin)
[![CI](https://github.com/sashite/pin.ex/actions/workflows/elixir.yml/badge.svg?branch=main)](https://github.com/sashite/pin.ex/actions)
[![License](https://img.shields.io/hexpm/l/sashite_pin.svg)](https://github.com/sashite/pin.ex/blob/main/LICENSE)

> **PIN** (Piece Identifier Notation) implementation for Elixir.

## Overview

This library implements the [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/).

PIN is a compact, ASCII-only token format encoding a **Piece Identity**: the tuple (**Piece Name**, **Piece Side**, **Piece State**, **Terminal Status**). Case encodes side, an optional `+`/`-` prefix encodes state, and an optional `^` suffix marks terminal pieces.

### Implementation Constraints

| Constraint | Value | Rationale |
|------------|-------|-----------|
| Token length | 1–3 characters | `[+-]?[A-Za-z]\^?` per spec |
| Character space | 312 tokens | 26 abbreviations × 2 sides × 3 states × 2 terminal |
| Function clauses | 312 + reject | All valid inputs resolved by compile-time pattern matching |

The closed domain of 312 possible values enables a compile-time generated architecture with zero branching overhead on the hot path.

## Installation

```elixir
# In your mix.exs
def deps do
  [
    {:sashite_pin, "~> 3.0"}
  ]
end
```

## Usage

### Parsing (String → Identifier)

Convert a PIN string into an `Identifier` struct.

```elixir
# Standard parsing (returns {:ok, _} or {:error, _})
{:ok, pin} = Sashite.Pin.parse("K")
pin.abbr       # => :K
pin.side       # => :first
pin.state      # => :normal
pin.terminal   # => false

# With state modifier
{:ok, pin} = Sashite.Pin.parse("+R")
pin.state  # => :enhanced

# With terminal marker
{:ok, pin} = Sashite.Pin.parse("K^")
pin.terminal  # => true

# Combined
{:ok, pin} = Sashite.Pin.parse("+K^")
pin.state      # => :enhanced
pin.terminal   # => true

# Bang version (raises on error)
pin = Sashite.Pin.parse!("+K^")

# Invalid input returns error tuple
{:error, reason} = Sashite.Pin.parse("")
{:error, reason} = Sashite.Pin.parse("invalid")
```

### Fetching by Components (Atoms → Identifier)

Retrieve an identifier directly by its components, bypassing string parsing entirely.

```elixir
# Direct lookup — no string parsing
pin = Sashite.Pin.fetch!(:K, :first)
pin.abbr  # => :K
pin.side  # => :first

# With explicit state and terminal
{:ok, pin} = Sashite.Pin.fetch(:R, :second, :enhanced, true)
pin.state     # => :enhanced
pin.terminal  # => true

# Defaults: state = :normal, terminal = false
{:ok, pin} = Sashite.Pin.fetch(:K, :first)

# Invalid components return error / raise
{:error, reason} = Sashite.Pin.fetch(:KK, :first)
Sashite.Pin.fetch!(:K, :third)  # => raises ArgumentError
```

### Formatting (Identifier → String)

Convert an `Identifier` back to a PIN string.

```elixir
pin = Sashite.Pin.parse!("+K^")
to_string(pin)  # => "+K^"

pin = Sashite.Pin.parse!("r")
to_string(pin)  # => "r"
```

### Validation

```elixir
# Boolean check (never raises)
Sashite.Pin.valid?("K")        # => true
Sashite.Pin.valid?("+R")       # => true
Sashite.Pin.valid?("K^")       # => true
Sashite.Pin.valid?("+K^")      # => true
Sashite.Pin.valid?("invalid")  # => false
Sashite.Pin.valid?(nil)        # => false
```

### Transformations

All transformations return new `Identifier` structs.

```elixir
pin = Sashite.Pin.parse!("K")

# State transformations
Sashite.Pin.Identifier.enhance(pin).state     # => :enhanced
Sashite.Pin.Identifier.diminish(pin).state    # => :diminished
Sashite.Pin.Identifier.normalize(pin).state   # => :normal

# Side transformation
Sashite.Pin.Identifier.flip(pin).side  # => :second

# Terminal transformations
Sashite.Pin.Identifier.terminal(pin).terminal      # => true
Sashite.Pin.Identifier.non_terminal(pin).terminal   # => false

# Attribute changes
Sashite.Pin.Identifier.with_abbr(pin, :Q).abbr            # => :Q
Sashite.Pin.Identifier.with_side(pin, :second).side        # => :second
Sashite.Pin.Identifier.with_state(pin, :enhanced).state    # => :enhanced
Sashite.Pin.Identifier.with_terminal(pin, true).terminal   # => true
```

### Queries

```elixir
pin = Sashite.Pin.parse!("+K^")

# State queries
Sashite.Pin.Identifier.normal?(pin)      # => false
Sashite.Pin.Identifier.enhanced?(pin)    # => true
Sashite.Pin.Identifier.diminished?(pin)  # => false

# Side queries
Sashite.Pin.Identifier.first_player?(pin)   # => true
Sashite.Pin.Identifier.second_player?(pin)  # => false

# Comparison queries
other = Sashite.Pin.parse!("k")
Sashite.Pin.Identifier.same_abbr?(pin, other)      # => true
Sashite.Pin.Identifier.same_side?(pin, other)       # => false
Sashite.Pin.Identifier.same_state?(pin, other)      # => false
Sashite.Pin.Identifier.same_terminal?(pin, other)   # => false
```

## API Reference

### Module Methods

```elixir
# Parses a PIN string into an Identifier.
# Returns {:ok, identifier} or {:error, reason}.
@spec Sashite.Pin.parse(String.t()) :: {:ok, Identifier.t()} | {:error, atom()}

# Parses a PIN string into an Identifier.
# Raises ArgumentError if the string is not valid.
@spec Sashite.Pin.parse!(String.t()) :: Identifier.t()

# Retrieves an Identifier by components.
# Bypasses string parsing entirely — validated by compile-time pattern matching.
# Returns {:ok, identifier} or {:error, reason}.
@spec Sashite.Pin.fetch(atom(), atom(), atom(), boolean()) :: {:ok, Identifier.t()} | {:error, atom()}

# Retrieves an Identifier by components.
# Raises ArgumentError if components are invalid.
@spec Sashite.Pin.fetch!(atom(), atom(), atom(), boolean()) :: Identifier.t()

# Reports whether string is a valid PIN identifier.
# Never raises; returns false for any invalid input.
@spec Sashite.Pin.valid?(term()) :: boolean()
```

### Identifier

```elixir
# Identifier represents a parsed PIN identifier with all attributes.
%Sashite.Pin.Identifier{
  abbr: :A..:Z,                              # Piece name abbreviation (always uppercase atom)
  side: :first | :second,                     # Piece side
  state: :normal | :enhanced | :diminished,   # Piece state
  terminal: boolean()                         # Terminal status
}
```

### Transformations

```elixir
# State transformations
@spec Identifier.enhance(Identifier.t()) :: Identifier.t()
@spec Identifier.diminish(Identifier.t()) :: Identifier.t()
@spec Identifier.normalize(Identifier.t()) :: Identifier.t()

# Side transformation
@spec Identifier.flip(Identifier.t()) :: Identifier.t()

# Terminal transformations
@spec Identifier.terminal(Identifier.t()) :: Identifier.t()
@spec Identifier.non_terminal(Identifier.t()) :: Identifier.t()

# Attribute changes
@spec Identifier.with_abbr(Identifier.t(), atom()) :: Identifier.t()
@spec Identifier.with_side(Identifier.t(), atom()) :: Identifier.t()
@spec Identifier.with_state(Identifier.t(), atom()) :: Identifier.t()
@spec Identifier.with_terminal(Identifier.t(), boolean()) :: Identifier.t()
```

### Queries

```elixir
# State queries
@spec Identifier.normal?(Identifier.t()) :: boolean()
@spec Identifier.enhanced?(Identifier.t()) :: boolean()
@spec Identifier.diminished?(Identifier.t()) :: boolean()

# Side queries
@spec Identifier.first_player?(Identifier.t()) :: boolean()
@spec Identifier.second_player?(Identifier.t()) :: boolean()

# Comparison queries
@spec Identifier.same_abbr?(Identifier.t(), Identifier.t()) :: boolean()
@spec Identifier.same_side?(Identifier.t(), Identifier.t()) :: boolean()
@spec Identifier.same_state?(Identifier.t(), Identifier.t()) :: boolean()
@spec Identifier.same_terminal?(Identifier.t(), Identifier.t()) :: boolean()
```

### Errors

Parsing errors are returned as atoms in `{:error, reason}` tuples:

| Atom | Cause |
|------|-------|
| `:empty_input` | String length is 0 |
| `:input_too_long` | String exceeds 3 characters |
| `:must_contain_one_letter` | Missing or multiple letters |
| `:invalid_state_modifier` | Invalid prefix character |
| `:invalid_terminal_marker` | Invalid suffix character |

## Security

This library is designed for backend use where inputs may come from untrusted clients. The implementation enforces a **zero dynamic atom creation** policy and bounded resource consumption.

### Atom table safety

The BEAM atom table is finite and not garbage-collected. Any library that calls `String.to_atom/1` on user input is a potential denial-of-service vector: an attacker can exhaust the atom table by sending unique strings, crashing the entire VM.

This implementation **never calls `String.to_atom/1` or `List.to_atom/1` at runtime**. Every atom that appears in an `Identifier` (`:A` through `:Z`, `:first`, `:second`, `:normal`, `:enhanced`, `:diminished`) is a compile-time literal embedded in the BEAM bytecode. Untrusted input cannot introduce new atoms into the system.

### Bounded resource consumption

All inputs are rejected at the byte level before any allocation occurs:

- **Length check first**: Inputs longer than 3 bytes are rejected before any byte inspection.
- **No regex engine**: Parsing uses raw binary pattern matching, eliminating ReDoS as an attack vector.
- **No intermediate allocations**: Valid inputs are dispatched to compile-time clauses that return pre-computed struct literals. Invalid inputs hit a catch-all clause that returns an error tuple. Neither path allocates temporary strings, lists, or atoms.

### Rejection guarantees

Any input that is not one of the 312 valid PIN tokens is rejected with an `{:error, reason}` tuple. The rejection path:

- does not raise exceptions (no backtrace capture cost),
- does not allocate atoms,
- does not allocate strings,
- executes in constant time (BEAM pattern match dispatch).

## Design Principles

- **Spec conformance**: Strict adherence to PIN v1.0.0
- **Compile-time code generation**: All 312 valid parse clauses are generated at compile time via metaprogramming — no runtime lookup tables, no branching chains
- **Binary pattern matching on the hot path**: The BEAM resolves `<<byte>>` pattern matches at native speed; no intermediate string operations
- **Zero dynamic atoms**: Every atom is a compile-time literal; `String.to_atom/1` is never called at runtime
- **Elixir idioms**: `{:ok, _}` / `{:error, _}` tuples, `parse!` / `fetch!` bang variants, `String.Chars` protocol
- **Immutable structs**: `Identifier` structs are immutable by design
- **No dependencies**: Pure Elixir standard library only

### Performance Architecture

PIN has a closed domain of exactly 312 valid tokens (26 letters × 2 cases × 3 states × 2 terminal). This implementation exploits that constraint through three complementary strategies.

**Compile-time clause generation** — A macro iterates over all 26 letters, 3 states, and 2 terminal values at compile time, emitting 312 explicit function clauses for `parse/1` — one per valid binary pattern. At runtime, the BEAM's pattern matching engine dispatches directly to the correct clause. There are no conditional branches, no map lookups, and no `Enum` traversals on the hot path — just a single function call resolved by the VM's optimized dispatch table.

**Raw binary matching** — Parsing operates on raw binary patterns (`<<byte>>`, `<<first, second>>`, `<<first, second, third>>`) rather than on string-level abstractions. This avoids `String.length/1`, `String.to_atom/1`, and other UTF-8-aware functions that carry overhead unnecessary for an ASCII-only specification. Each valid clause destructures the bytes directly and returns pre-computed struct literals containing only compile-time atoms.

**Dual-path API** — Parsing is split into two layers to avoid using exceptions for control flow:

- **Safe layer** — `parse/1` and `fetch/4` perform all validation and return `{:ok, identifier}` on success or `{:error, reason}` on failure, without raising, without allocating exception objects, and without capturing backtraces.
- **Bang layer** — `parse!/1` and `fetch!/4` call the safe variants internally. On failure, they raise `ArgumentError` exactly once, at the boundary. `valid?/1` calls `parse/1` and returns a boolean directly, never raising.

**Direct component lookup** — `fetch/4` bypasses string parsing entirely. Given atoms `(:K, :first, :enhanced, true)`, it validates the components through compile-time generated pattern matching clauses — the same dispatch mechanism as `parse/1`, applied to structured data. This is the fastest path for callers that already have structured data (e.g., EPIN or FEEN's parser reconstructing identifiers from internal attributes).

This architecture ensures that PIN never becomes a bottleneck when called from higher-level parsers like EPIN or FEEN, where it may be invoked hundreds of times per position.

## Related Specifications

- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
- [PIN Specification](https://sashite.dev/specs/pin/1.0.0/) — Official specification
- [PIN Examples](https://sashite.dev/specs/pin/1.0.0/examples/) — Usage examples

## License

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