<h1 align="center">
<img width="99" alt="Rust logo" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg">
<br>
<b>span-lang</b>
<br>
<sub><sup>SOURCE SPANS & POSITION MAPPING</sup></sub>
</h1>
<div align="center">
<a href="https://crates.io/crates/span-lang"><img alt="Crates.io" src="https://img.shields.io/crates/v/span-lang"></a>
<a href="https://crates.io/crates/span-lang"><img alt="Downloads" src="https://img.shields.io/crates/d/span-lang?color=%230099ff"></a>
<a href="https://docs.rs/span-lang"><img alt="docs.rs" src="https://img.shields.io/docsrs/span-lang"></a>
<a href="https://github.com/jamesgober/span-lang/actions"><img alt="CI" src="https://github.com/jamesgober/span-lang/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://github.com/rust-lang/rfcs/blob/master/text/2495-min-rust-version.md"><img alt="MSRV" src="https://img.shields.io/badge/MSRV-1.85%2B-blue"></a>
</div>
<br>
<div align="left">
<p>
span-lang provides the source-position substrate for language tooling: compact byte-offset spans, line/column resolution, and multi-file coordinate mapping. It is the foundational crate every later stage of a lexer, parser, or compiler references when reporting where something lives in source.
</p>
<br>
<hr>
<p>
<strong>MSRV is 1.85+</strong> (Rust 2024 edition).
</p>
<blockquote>
<strong>Status: surface frozen (pre-1.0).</strong> The full position, span, resolution, and <code>Spanned<T></code> surface is implemented and property-tested as of <code>v0.4.0</code>, with the <code>O(log lines)</code> lookup verified by benchmark scaling. The public API is now frozen — only docs, tests, and internal optimisation remain before the <code>1.0.0</code> stability tag. See <a href="./docs/API.md#stability"><code>API.md</code></a> and <a href="./CHANGELOG.md"><code>CHANGELOG.md</code></a>.
</blockquote>
</div>
<hr>
<br>
## Installation
```toml
[dependencies]
span-lang = "0.4"
```
`no_std` targets disable the default `std` feature; the crate then relies only on `core` and `alloc`:
```toml
span-lang = { version = "0.4", default-features = false }
```
<br>
<hr>
<br>
## Performance
A `Span` is a `Copy` value (two packed 32-bit offsets, eight bytes), and line/column resolution is a binary search over line starts — `O(log lines)`, never a re-scan of the source. Latest local Criterion means (`cargo bench`, Windows x86_64, Rust stable):
- **`Span::merge`** — ~0.6 ns/op
- **`LineIndex::offset`** (line/col → byte) — ~2.5 ns/op
- **`LineIndex::line_col`** (byte → line/col) — ~8.7 ns/op
- **`LineIndex::new`** — ~8.4 µs to index 1 000 lines (the only `O(n)` operation; lookups allocate nothing)
<br>
<hr>
<br>
## Features
- **`BytePos`** — a 4-byte `Copy` byte offset; the atom every span is built from.
- **`Span`** — a half-open `start..end` byte range with `len`, `is_empty`, `contains`, ordering, and an associative, commutative `merge`. The `start <= end` invariant is enforced at construction.
- **`LineCol`** — a resolved 1-based line/column, where the column counts Unicode scalar values (never bytes, never inside a multi-byte sequence).
- **`LineIndex`** — built once per source; maps `BytePos` ↔ `LineCol` in `O(log lines)`, handling `\n` and `\r\n` uniformly, with no allocation on the lookup path. Also yields a line's text span for rendering.
- **`Spanned<T>`** — a value paired with the span it came from, so a token or AST node carries its location without the value type knowing about positions.
An optional `serde` feature round-trips every position type; `Span` deserialises through its constructor, so a value from an untrusted source cannot violate the `start <= end` invariant. Correctness is held to the [project invariants](./docs/API.md#invariants) by property tests cross-checked against a naive reference resolver over UTF-8 input including multi-byte characters and CRLF.
<br>
## Usage
```rust
use span_lang::{LineIndex, Span};
let src = "fn main() {\n work();\n}\n";
// Spans are half-open byte ranges; merge covers both inputs.
let call = Span::new(16, 22);
assert_eq!(call.len(), 6);
// Resolve a byte offset to a human (line, column) coordinate.
let index = LineIndex::new(src);
let lc = index.line_col(call.start());
assert_eq!((lc.line, lc.col), (2, 5));
// The mapping is reversible.
assert_eq!(index.offset(lc), Some(call.start()));
```
<br>
## API Overview
For a complete reference with examples, see [`docs/API.md`](./docs/API.md).
- [`BytePos`](./docs/API.md#bytepos) — a byte offset into one source.
- [`Span`](./docs/API.md#span) — a half-open byte range with `merge`, `contains`, and ordering.
- [`LineCol`](./docs/API.md#linecol) — a resolved 1-based line/column coordinate.
- [`LineIndex`](./docs/API.md#lineindex) — byte ↔ line/column resolution in `O(log lines)`, plus per-line text spans.
- [`Spanned<T>`](./docs/API.md#spanned) — a value paired with its `Span`.
<hr>
<br>
## Contributing
See <a href="./dev/DIRECTIVES.md"><code>dev/DIRECTIVES.md</code></a> for engineering standards and the definition of done. Before a PR: `cargo fmt --all`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test --all-features` must be clean.
<br>
<div id="license">
<h2>License</h2>
<p>Licensed under either of</p>
<ul>
<li><b>Apache License, Version 2.0</b> — <a href="./LICENSE-APACHE">LICENSE-APACHE</a></li>
<li><b>MIT License</b> — <a href="./LICENSE-MIT">LICENSE-MIT</a></li>
</ul>
<p>at your option.</p>
</div>
<div align="center">
<h2></h2>
<sup>COPYRIGHT <small>©</small> 2026 <strong>James Gober <me@jamesgober.com>.</strong></sup>
</div>