Skip to content

sashite/sin.ex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sin.ex

Hex.pm Docs CI License

SIN (Style Identifier Notation) implementation for Elixir.

Overview

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.

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

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

Usage

Parsing (String → Identifier)

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

Safe Fetching by Components (Atom, Atom → Identifier)

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 ArgumentError

Formatting (Identifier → String)

Convert 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"

Validation

# 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

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

# 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

# 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

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

License

Available as open source under the Apache License 2.0.

About

SIN (Style Identifier Notation) implementation for Elixir with immutable style objects.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages