v0.4.0 — Spanned<T>, serde, feature freeze
Pre-releasespan-lang v0.4.0 — Spanned<T>, serde, feature freeze
The surface, completed and frozen. v0.4.0 adds the last two pieces of the
0.x design — the Spanned<T> wrapper that carries a value alongside its span, and
an optional serde feature that round-trips every position type — and then
declares the public API frozen. From here to 1.0.0 there are no new or changed
public items, only documentation, tests, and internal optimisation. No breaking
changes.
What is span-lang?
The source-position substrate for language tooling. It defines the small,
copyable coordinate types that a lexer, parser, and diagnostic renderer all
share: a byte position, a byte-offset span, a resolved line/column, and the index
that maps between them — correctly over UTF-8, across \n and \r\n. It owns
positions and nothing else.
What's new in 0.4.0
Spanned<T> — a value and where it came from
A parser threads spans through every token and node. Spanned<T> is the wrapper
that lets a value carry its location without the value type itself knowing
anything about positions — Copy whenever T is, ordered by span first so a
slice of nodes sorts into source order.
use span_lang::{Span, Spanned};
// A lexeme: the text "10" at bytes 2..4.
let raw = Spanned::new(Span::new(2, 4), "10");
// `map` lifts it into a typed node, keeping the span.
let parsed = raw.map(|s| s.parse::<u32>().unwrap());
assert_eq!(parsed.value, 10);
assert_eq!(parsed.span, Span::new(2, 4));map transforms the value and preserves the span; as_ref borrows the value
(yielding Spanned<&T>, mirroring Option::as_ref) so you can inspect or map it
without consuming the original; Display renders value @ start..end.
serde — round-tripping positions
Behind the opt-in serde feature, every position type derives Serialize and
Deserialize:
BytePosis#[serde(transparent)]— a bare number on the wire, not a wrapper
object.LineColandSpanned<T>serialise as structs of their public fields;
Spanned<T>round-trips for any serialisableT.Spanhas a hand-writtenDeserializethat routes the incoming{ start, end }
throughSpan::new, so a span read from an untrusted source upholds the
start <= endinvariant exactly as a constructed one does. An inverted pair on
the wire is normalised, never accepted as-is — input is validated at the
boundary, not after.
use span_lang::Span;
// Inverted on the wire → normalised on the way in.
let s: Span = serde_json::from_str(r#"{"start":10,"end":4}"#).unwrap();
assert_eq!(s, Span::new(4, 10));
assert!(s.start() <= s.end());tests/serde.rs round-trips BytePos, Span, LineCol, and Spanned<T> (over
both an owned String and a scalar) through serde_json, and asserts the
inverted-span normalisation. LineIndex is deliberately not serialisable: it
borrows a source and is rebuilt from text, not restored from bytes.
The surface is frozen
With Spanned<T> and serde in place, the public API is complete and now
frozen. docs/API.md carries a Stability
section recording the frozen surface and the SemVer promise from 1.0.0: no
public item removed or changed incompatibly before 2.0.0; the serde wire
representations and the Display formats are part of the contract; an MSRV
increase is a minor change. The remaining road to 1.0.0 is the full
property-test and benchmark suite green on all three platforms, and nothing else.
Breaking changes
None. Spanned<T> and the serde feature are purely additive; the feature is
off by default and has no effect on the API when disabled. Every v0.3.0 program
compiles and behaves identically.
Verification
Run on Windows x86_64, Rust stable 1.95.x and MSRV 1.85; identical commands pass
on Linux (WSL2 Ubuntu) and via the configured CI matrix:
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --no-default-features --all-targets -- -D warnings
cargo test
cargo test --all-features
cargo build --no-default-features
cargo build --no-default-features --features serde
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +1.85 build --all-features
cargo audit
cargo deny checkAll green. Counts at this tag:
- Default features: 32 unit + 7 property tests + 26 doctests.
--all-features: 32 unit + 7 property tests + 7 serde tests + 26 doctests.
What's next
- 1.0.0 — API freeze. The frozen surface stabilised and tagged:
docs/API.md
marked stable, the SemVer promise in force, and the full property-test and
benchmark suite green on Linux, macOS, and Windows.
Installation
[dependencies]
span-lang = "0.4"
# With serialisation:
span-lang = { version = "0.4", features = ["serde"] }MSRV: Rust 1.85.
Documentation
Full diff: v0.3.0...v0.4.0.
Changelog: CHANGELOG.md.