# sin.ex
[](https://hex.pm/packages/sashite_sin)
[](https://hexdocs.pm/sashite_sin)
[](https://github.com/sashite/sin.ex/actions)
[](https://github.com/sashite/sin.ex/blob/main/LICENSE)
> **SIN** (Style Identifier Notation) implementation for Elixir.
## Overview
This library implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/).
SIN is a single-character, ASCII-only token format encoding a **Player Identity**: the tuple (**Player Side**, **Player Style**). Uppercase indicates the first player, lowercase indicates the second player.
### Implementation Constraints
| Constraint | Value | Rationale |
|------------|-------|-----------|
| Token length | Exactly 1 | Single ASCII letter per spec |
| Character space | A–Z, a–z | 52 total tokens (26 abbreviations × 2 sides) |
| Function clauses | 52 + reject | All valid inputs resolved by compile-time pattern matching |
The closed domain of 52 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_sin, "~> 3.1"}
]
end
```
## Usage
### Parsing (String → Identifier)
Convert a SIN string into an `Identifier` struct.
```elixir
# Standard parsing (returns {:ok, _} or {:error, _})
{:ok, sin} = Sashite.Sin.parse("C")
sin.abbr # => :C
sin.side # => :first
# Lowercase indicates second player
{:ok, sin} = Sashite.Sin.parse("c")
sin.abbr # => :C
sin.side # => :second
# Bang version (raises on error)
sin = Sashite.Sin.parse!("C")
# Invalid input returns error tuple
{:error, :not_a_string} = Sashite.Sin.parse(nil)
{:error, :empty_input} = Sashite.Sin.parse("")
{:error, :input_too_long} = Sashite.Sin.parse("CC")
```
### Safe Fetching by Components (Atom, Atom → Identifier)
Retrieve an identifier directly by abbreviation and side, bypassing string parsing entirely.
```elixir
# Direct lookup — no string parsing
sin = Sashite.Sin.fetch!(:C, :first)
sin.abbr # => :C
sin.side # => :first
# Safe variant
{:ok, sin} = Sashite.Sin.fetch(:C, :second)
sin.side # => :second
# Invalid components return error / raise
{:error, reason} = Sashite.Sin.fetch(:CC, :first)
Sashite.Sin.fetch!(:C, :third) # => raises ArgumentError
```
### Formatting (Identifier → String)
Convert an `Identifier` back to a SIN string.
```elixir
sin = Sashite.Sin.parse!("C")
to_string(sin) # => "C"
sin = Sashite.Sin.parse!("c")
to_string(sin) # => "c"
```
### Validation
```elixir
# Boolean check (never raises)
Sashite.Sin.valid?("C") # => true
Sashite.Sin.valid?("c") # => true
Sashite.Sin.valid?("") # => false
Sashite.Sin.valid?("CC") # => false
Sashite.Sin.valid?("1") # => false
Sashite.Sin.valid?(nil) # => false
```
### Queries
```elixir
sin = Sashite.Sin.parse!("C")
# Side queries
Sashite.Sin.Identifier.first_player?(sin) # => true
Sashite.Sin.Identifier.second_player?(sin) # => false
# Comparison queries
other = Sashite.Sin.parse!("c")
Sashite.Sin.Identifier.same_abbr?(sin, other) # => true
Sashite.Sin.Identifier.same_side?(sin, other) # => false
```
## API Reference
### Module Methods
```elixir
# Parses a SIN string into an Identifier.
# Returns {:ok, identifier} or {:error, reason}.
@spec Sashite.Sin.parse(String.t()) :: {:ok, Identifier.t()} | {:error, atom()}
# Parses a SIN string into an Identifier.
# Raises ArgumentError if the string is not valid.
@spec Sashite.Sin.parse!(String.t()) :: Identifier.t()
# Retrieves an Identifier by abbreviation and side.
# Bypasses string parsing entirely.
# Returns {:ok, identifier} or {:error, reason}.
@spec Sashite.Sin.fetch(atom(), atom()) :: {:ok, Identifier.t()} | {:error, atom()}
# Retrieves an Identifier by abbreviation and side.
# Raises ArgumentError if components are invalid.
@spec Sashite.Sin.fetch!(atom(), atom()) :: Identifier.t()
# Reports whether string is a valid SIN identifier.
# Never raises; returns false for any invalid input.
@spec Sashite.Sin.valid?(term()) :: boolean()
```
### Identifier
```elixir
# Identifier represents a parsed SIN identifier with abbreviation and side.
%Sashite.Sin.Identifier{
abbr: :A..:Z, # Style abbreviation (always uppercase atom)
side: :first | :second # Player side
}
```
### Queries
```elixir
# 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()
```
### Errors
Parsing errors are returned as atoms in `{:error, reason}` tuples:
| Atom | Cause |
|------|-------|
| `:not_a_string` | Input is not a binary (e.g., `nil`, integer, atom, list) |
| `:empty_input` | String length is 0 |
| `:input_too_long` | String exceeds 1 character |
| `:must_be_letter` | Character is not A-Z or a-z |
| `:invalid_abbr` | Abbreviation is not `:A` through `:Z` |
| `:invalid_side` | Side is not `:first` or `:second` |
## Design Principles
- **Spec conformance**: Strict adherence to SIN v1.0.0
- **Compile-time code generation**: All 52 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
- **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
SIN has a closed domain of exactly 52 valid tokens (26 letters × 2 cases). This implementation exploits that constraint through three complementary strategies.
**Compile-time clause generation** — A macro iterates over the 26 letters at compile time and emits 52 explicit function clauses for `parse/1` (one per valid byte value). 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 the raw binary `<<byte>>` 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 single byte directly and returns pre-computed atoms.
**Dual-path API** — Parsing is split into two layers to avoid using exceptions for control flow:
- **Safe layer** — `parse/1` and `fetch/2` 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!/2` 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/2` bypasses string parsing entirely. Given atoms `(:C, :first)`, it validates the components and builds the struct directly. This is the fastest path for callers that already have structured data (e.g., FEEN's parser reconstructing a style–turn field from internal attributes).
This architecture ensures that SIN never becomes a bottleneck when called from higher-level parsers like FEEN, where it may be invoked multiple times per position.
## Related Specifications
- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
- [SIN Specification](https://sashite.dev/specs/sin/1.0.0/) — Official specification
- [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/) — Usage examples
## License
Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).