Record that a write through a string literal fails two ways

At -O0 it stores into read-only memory and takes SIGSEGV; at -O2 LLVM deletes
it as undefined and the program prints the unmodified string and exits 0. Same
source, and which way it fails depends on a flag, which is worse than either
outcome on its own.

Nothing refuses it and nothing cheaply can: bytes turns a string into a [u8],
the language lets you write through a slice, and by then nothing records where
the bytes came from. That is provenance, which plan.org defers as open decision
#3. Emitting literals as mutable globals is not a fix - it moves which flag
misbehaves and costs their read-only placement.

Written down rather than half-fixed, with the rule the string lane already
follows: a function over a string must not write through it.
This commit is contained in:
Joseph Ferano 2026-09-12 04:04:28 +07:00
parent ee5abd40fc
commit 0cac22a5ef

21
NEXT.md
View File

@ -446,6 +446,27 @@ so wasm32 needs no exception proposal.
## Sharp edges
- **Writing through a string literal is undefined, and the two build modes
disagree about how.** `(let [s (bytes "Hi")] (set (at s 0) \h))` stores into
a `private unnamed_addr constant`. At `-O0` that is a store to read-only
memory and the program takes SIGSEGV; at `-O2` LLVM deletes it as undefined
and the program prints `Hi` and exits 0. Same source, and which way it fails
depends on a flag — the worst shape available, and worse than either outcome
alone.
Nothing refuses it. `bytes` turns a `string` into a `[u8]`, the language lets
you write through a slice, and by then nothing records that the bytes came
from a constant. The honest fix is provenance — knowing a slice's origin —
which is plan.org open decision #3 and deliberately deferred. A cheaper one
that is *not* a fix: emitting literals as mutable globals only moves which
flag misbehaves, and costs their read-only placement.
Found by the string lane while deciding whether `lower-ascii` should mutate
in place. It ships the copying version for exactly this reason, and that is
the rule to follow until provenance exists: **a function over a `string` must
not write through it.**
Most of these are edges the language keeps and you should know about. Two — the top-level namespace and the shift count,
both found by review after milestone 4 — were bugs that reached LLVM or ran wrong, and are **fixed**; each says so. They
stay written down because each one is now a rule the checker enforces, and a later change could quietly drop it.