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.
MSRV is 1.85+ (Rust 2024 edition).
Status: stable (v1.0.0). The full position, span, resolution, andSpanned<T>surface is implemented and property-tested, with theO(log lines)lookup verified by benchmark scaling. The public API is frozen under the SemVer promise: no item is removed or changed incompatibly before2.0.0. SeeAPI.mdandCHANGELOG.md.
[dependencies]
span-lang = "1"no_std targets disable the default std feature; the crate then relies only on core and alloc:
span-lang = { version = "1", default-features = false }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/opLineIndex::offset(line/col → byte) — ~2.5 ns/opLineIndex::line_col(byte → line/col) — ~8.7 ns/opLineIndex::new— ~8.4 µs to index 1 000 lines (the onlyO(n)operation; lookups allocate nothing)
BytePos— a 4-byteCopybyte offset; the atom every span is built from.Span— a half-openstart..endbyte range withlen,is_empty,contains, ordering, and an associative, commutativemerge. Thestart <= endinvariant 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; mapsBytePos↔LineColinO(log lines), handling\nand\r\nuniformly, 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 by property tests cross-checked against a naive reference resolver over UTF-8 input including multi-byte characters and CRLF.
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()));For a complete reference with examples, see docs/API.md.
BytePos— a byte offset into one source.Span— a half-open byte range withmerge,contains, and ordering.LineCol— a resolved 1-based line/column coordinate.LineIndex— byte ↔ line/column resolution inO(log lines), plus per-line text spans.Spanned<T>— a value paired with itsSpan.
See dev/DIRECTIVES.md 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.
Licensed under either of
- Apache License, Version 2.0 — LICENSE-APACHE
- MIT License — LICENSE-MIT
at your option.