SIN (Style Identifier Notation) implementation for Elixir.
This library implements the SIN Specification v1.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.
| 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.
# In your mix.exs
def deps do
[
{:sashite_sin, "~> 3.1"}
]
endConvert a SIN string into an Identifier struct.
# 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")Retrieve an identifier directly by abbreviation and side, bypassing string parsing entirely.
# 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 ArgumentErrorConvert an Identifier back to a SIN string.
sin = Sashite.Sin.parse!("C")
to_string(sin) # => "C"
sin = Sashite.Sin.parse!("c")
to_string(sin) # => "c"# 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) # => falsesin = 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# 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 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
}# 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()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 |
- 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.Charsprotocol - Immutable structs:
Identifierstructs are immutable by design - No dependencies: Pure Elixir standard library only
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/1andfetch/2perform 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!/1andfetch!/2call the safe variants internally. On failure, they raiseArgumentErrorexactly once, at the boundary.valid?/1callsparse/1and 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.
- Game Protocol — Conceptual foundation
- SIN Specification — Official specification
- SIN Examples — Usage examples
Available as open source under the Apache License 2.0.