# source-lang v0.4.0 — serde & feature freeze
**The surface is set.** v0.2.0 built the core, v0.3.0 made it loadable. v0.4.0 adds
the last planned capability — optional `serde` so a whole source map can be
persisted and restored — and then freezes the public API. From here to `1.0.0`
there are no new public items, only documentation, tests, and internal work; `1.0.0`
ratifies this surface and holds it stable until `2.0`.
## What is source-lang?
The multi-file coordinate layer of a compiler front-end. It loads files and
in-memory buffers, gives each a stable `SourceId`, lays them out in one global
position space, and maps any global position back to the source and local offset —
or the file, line, and column — it came from. It is the layer above `span-lang`: a
`Span` says *where in a buffer* an error is, source-lang says *which buffer*. It owns
source storage and coordinate mapping only.
## What's new in 0.4.0
### `serde` for the source map
With the `serde` feature, `SourceMap` and `SourceId` implement `Serialize` and
`Deserialize`, and the re-exported coordinate types carry their `span-lang` serde
implementations. A map round-trips through any serde format:
```rust,ignore
let mut map = SourceMap::new();
map.add("main.rs", "fn main() {}")?;
map.add("util.rs", "let x = 1;\nlet y = 2;")?;
let json = serde_json::to_string(&map)?;
let restored: SourceMap = serde_json::from_str(&json)?;
assert_eq!(map, restored);
assert_eq!(restored.line_col(BytePos::new(23)).unwrap().1.line, 2);
```
The wire form is deliberately small: the list of sources — each a name and its text
— plus the size ceiling. Everything the map derives (each source's span, its id, the
global high-water mark) is left out and **regenerated on load**, by replaying the
sources through the same insertion path as `add`.
That choice is what makes deserialisation safe. The layout is rebuilt, not trusted,
so a hand-edited or corrupted document cannot smuggle in overlapping ranges or a
broken coordinate space; a source list whose combined length overruns the 32-bit
space is a deserialisation error, not a silently broken map. The `max_source_len`
field is optional on read, so a document written by an earlier build still loads.
A `SourceId` serialises transparently as its `u32` index, so a handle stored in an
AST node or a cached diagnostic round-trips on its own.
### `PartialEq` / `Eq` for `SourceMap`
Two maps compare equal when they hold the same sources in the same layout with the
same ceiling — useful for tests and for confirming a round-trip preserved the map.
### Public API frozen
This release declares the public surface frozen. No public items will be added or
changed before `1.0.0`. `docs/API.md` is marked accordingly. The frozen surface is
the complete source map: construction, the three loaders (`add`, `add_bytes`,
`add_file`), resolution (`locate`, `line_col`, `source`, `iter`), the size ceiling,
`SourceFile` and its `line_index`, `SourceId`, `SourceMapError`, and the re-exported
coordinate types.
## Breaking changes
**None.** Everything in this release is additive: the new trait implementations are
gated behind the `serde` feature, and `PartialEq`/`Eq` add no constraints to existing
code.
## Verification
Run on Windows x86_64 (Rust stable and 1.85 MSRV); the same commands run on Linux
(WSL2 Ubuntu) and across the CI matrix on Linux, macOS, and Windows:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
cargo build --no-default-features
cargo +1.85 build --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo audit
cargo deny check
```
All green. Counts at this tag:
- `--all-features`: 27 unit + 6 file-loading + 5 property + 5 serde + 23 doctests.
- Default features: identical minus the serde integration tests.
The serde round-trip is covered in `tests/serde.rs`: a populated map (including a
multi-byte source and a zero-width source) survives a JSON round-trip with its ids,
spans, and line/column resolution intact, a bare `SourceId` round-trips on its own,
and a document missing the ceiling field loads with the default.
## What's next
- **1.0.0 — API freeze.** The surface declared frozen here becomes the stable `1.0`
contract: documentation, the full property-test and benchmark suite green on all
three platforms, and a recorded SemVer promise.
## Installation
```toml
[dependencies]
source-lang = "0.4"
```
Enable serialization with:
```toml
[dependencies]
source-lang = { version = "0.4", features = ["serde"] }
```
MSRV: Rust 1.85.
## Documentation
- [README](https://github.com/jamesgober/source-lang/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/source-lang/blob/main/docs/API.md)
- [CHANGELOG](https://github.com/jamesgober/source-lang/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.3.0...v0.4.0`](https://github.com/jamesgober/source-lang/compare/v0.3.0...v0.4.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/source-lang/blob/main/CHANGELOG.md#040---2026-06-20).