Skip to content

jamesgober/span-lang

Repository files navigation

Rust logo
span-lang
SOURCE SPANS & POSITION MAPPING

Crates.io Downloads docs.rs CI MSRV

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, and Spanned<T> surface is implemented and property-tested, with the O(log lines) lookup verified by benchmark scaling. The public API is frozen under the SemVer promise: no item is removed or changed incompatibly before 2.0.0. See API.md and CHANGELOG.md.


Installation

[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 }



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)



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 BytePosLineCol 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 by property tests cross-checked against a naive reference resolver over UTF-8 input including multi-byte characters and CRLF.


Usage

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()));

API Overview

For a complete reference with examples, see docs/API.md.

  • BytePos — a byte offset into one source.
  • Span — a half-open byte range with merge, contains, and ordering.
  • LineCol — a resolved 1-based line/column coordinate.
  • LineIndex — byte ↔ line/column resolution in O(log lines), plus per-line text spans.
  • Spanned<T> — a value paired with its Span.


Contributing

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.


License

Licensed under either of

at your option.

COPYRIGHT © 2026 James Gober .

About

Source positions, spans, and byte-to-line/column mapping for language tooling.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

0 stars

Watchers

1 watching

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages