Skip to content

sashite/pin.ex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pin.ex

Hex.pm Docs CI License

PIN (Piece Identifier Notation) implementation for Elixir.

Overview

This library implements the PIN Specification v1.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

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

Usage

Parsing (String → Identifier)

Convert a PIN string into an Identifier struct.

# 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.

# 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.

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

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

Validation

# 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.

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

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

# 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

# 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

# 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

# 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 layerparse/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 layerparse!/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 lookupfetch/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

License

Available as open source under the Apache License 2.0.

About

PIN (Piece Identifier Notation) implementation for Elixir with immutable identifier objects.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages