memori (目盛) — the graduation marks an offset was counted in.
The problem this vocabulary corners
An offset into text is a usize, and a usize says nothing about which
ruler it was measured on. escriba measures on three, and they disagree on
every non-ASCII character:
| scale | "héllo" end |
who demands it |
|---|---|---|
| bytes | 6 | regex, every &str index |
| chars | 5 | escriba's own Position/match offsets |
| UTF-16 units | 5 | LSP, and anything speaking to an editor client |
Passing one where another is expected compiles perfectly and is wrong only for users with non-ASCII text — the worst possible failure profile, because it survives every test written in English.
Five distinct bugs in one session were this class. That is the signal a primitive is announcing itself rather than five tasks:
- a commit stepping from the wrong anchor,
saturating_sub(1)making offset 0 unreachable,- match offsets used against text that had since been edited,
- byte/char/UTF-16 conversion correct only because one function guarded it,
- an exclusive-vs-inclusive endpoint chosen by picking a function name.
The three axes, made orthogonal
- Scale — [
Offset<S>] is phantom-tagged, soOffset<Bytes>andOffset<Chars>are different types and mixing them is a compile error. Conversion is possible only through a [Ruler], which cannot exist without the text it measures. - Bound — [
Bound] makes "does the endpoint count itself?" a value the caller states, not a function name they must pick correctly, and not an arithmetic fudge at the call site. - Freshness — [
Anchored] carries the [EditGen] an offset was computed against, so using it after an edit is aNonerather than a silently wrong column.
Tier honesty
Scale-mixing is truly unrepresentable (a type error, E0308). Bound and
freshness are parse-time-rejected at this border — you can still build a
Ruler for the wrong text. The full ledger is in docs/memori.md.
The (defmemori …) tatara-lisp surface is a NAMED FOLLOW-UP, not shipped.
This crate is the typed Rust border only.
Why a leaf crate
It has no dependencies, deliberately. escriba-core imports
escriba_search::Direction, so core depends on SEARCH — and a positioning
primitive living in core would be invisible to the search engine, which is
where the step/step_inclusive twins [Bound] exists to replace live.
The vocabulary has to sit below both, so it does.