# String Representation
Deft strings are **codepoint-indexed over a byte backing store** — the
Emacs model. `len` counts characters, `$s(a..b)` slices characters,
and slicing can never land mid-character; internally the string is an
immutable UTF-8 byte array that also safely holds arbitrary binary.
This entry explains the coordinate systems, why the byte layer stays
visible through `bytes/*`, and how display width fits in — so questions
like "why are there three length-ish functions" read as designed
behaviour.
## One type, three coordinate systems
| Coordinate | Unit | Operations |
|---|---|---|
| **Character index** | codepoint | `len`, `$s(a..b)` / `slice`, `nth`, `char-at`, `index-of`, `split ""`, `reverse`, `chars/count`, `chars/slice`, `re/find` start/end |
| **Display column** | terminal cell (East Asian Wide = 2) | `width`, `cells`, `truncate-width`, `pad-width-left/right`, `wrap-width` |
| **Byte offset** | raw byte | `bytes/len`, `bytes/slice`, `byte-at` |
Character indexing is the *editing* coordinate — the editor's cursor,
selection, and find offsets are character indices, and all string
verbs agree with each other on it. Display columns exist because the
cell grid is monospace: a wide glyph (中文) occupies two cells, so
"column on screen" is a different number than "character index", and
layout verbs compute it. Byte offsets exist because strings double as
the binary type: `fs/read` of a PNG, crypto blobs, protocol bodies.
## Why indexing is codepoint-based
Before this design, string indexing was byte-based — inherited from
the byte array — which meant slicing a `─` (three UTF-8 bytes)
produced its lead byte `0xE2` (rendered as `â`): one character could
come through as three fragments of mojibake. Every consumer had to
bridge by hand. The migration moved the semantics into the language:
- `s[i]`, `$s(a..b)`, `index-of` results, and `len` all speak
characters — the bridge disappeared, and the editor's cursor model
became a plain integer with no boundary-snapping.
- Mojibake is structurally impossible: a slice boundary that would
land inside a character resolves to the character's edge.
The trade-offs accepted: indexing a character position requires
resolving it to a byte offset (a scan — O(n); fine at line/label
sizes, and `len` itself is O(1) via a count memoized on the string
object, exactly like the hash cache). And "character" is a codepoint,
not a grapheme cluster — `é` written as `e` + combining accent is two
characters, and emoji ZWJ sequences are several. Grapheme segmentation
is a possible future display-layer addition, not an indexing change.
## The binary layer
Strings hold arbitrary bytes; that will not change (there is no
separate bytes type). The rule is that **binary data uses the
`bytes/*` verbs for offsets and sizing**:
set bin [fs/read "image.png"] set chunk [bytes/slice $bin 0 4096] # exact bytes — never re-aligned if ([bytes/len $bin] < 16) { ... }
The reason is sharp: binary data can contain accidental valid UTF-8
runs (three bytes `e4 b8 80` inside a PNG decode as 中). Under
character slicing those three bytes are ONE character — chunk
boundaries computed as character counts would shift and corrupt the
data. Byte math on binary must be byte math. The healthy pattern
already used throughout the packages: pass binary strings WHOLE to
native functions (crypto, codecs, media) and never index into them
from Deft.
Text, conversely, should never touch `bytes/*` — it reads text through
characters and lays out through widths.
## Immutability and caches
Strings are immutable after creation (single allocation: header +
inline bytes). Two values are memoized lazily on the object, following
the same pattern: `hash` (for map keys) and the character count (for
`len`). Both are computed at most once per string, on the heap that
owns it, written through a const-cast — safe because the value is a
pure function of the immutable bytes.
## Rendering: where characters become columns
The TUI hosts draw text as a grid of monospace cells. The renderer
walks a line's `[cells]` — per character: its index, its string, its
display width — writing wide glyphs as a two-cell pair. The editor
maps its cursor (a character index) to a screen column through the
same cells; clicks map back. See [Font Rendering](font-rendering) for
the atlas side of that pipeline.