Skip to content

Commit 0888411

Browse files
committed
Merge origin/main into PR #140
Resolve backend-assurance coverage conflicts after PR #139 and keep all four primitive harnesses listed as validated. Adjust BEAM harness checks to measure codepoint counts explicitly instead of Elixir grapheme counts.
2 parents 4d37a81 + 8638068 commit 0888411

8 files changed

Lines changed: 623 additions & 45 deletions

File tree

PROOF-NEEDS.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ Classification key: **(J)** genuinely unavoidable, documented as an axiom;
2525

2626
| # | Site | Function | Type | Class | Rationale |
2727
|---|------|----------|------|-------|-----------|
28-
| 1 | `SafetyLemmas.idr:53` | `charEqSound` | `(c1,c2:Char) -> c1 == c2 = True -> c1 = c2` | **J** | `Char` is an opaque primitive; `==` is `prim__eqChar` (foreign `Bool`). Idris2 0.8.0 has no in-language soundness principle for primitive equality. Standard, well-understood axiom. |
29-
| 2 | `SafetyLemmas.idr:60` | `charEqSym` | `(x,y:Char) -> (x == y) = (y == x)` | **J** | Symmetry of `prim__eqChar`. Same reason as #1 — opaque primitive, no decision procedure to recurse on. |
28+
| 1 | `SafetyLemmas.idr:60` | `charEqSound` | `(c1,c2:Char) -> c1 == c2 = True -> c1 = c2` | **J** | `Char` is an opaque primitive; `==` is `prim__eqChar` (foreign `Bool`). Idris2 0.8.0 has no in-language soundness principle for primitive equality. Standard, well-understood axiom. **Externally validated** via backend-assurance harness (`docs/backend-assurance/prim__eqChar.md` + BEAM property test). |
29+
| 2 | `SafetyLemmas.idr:67` | `charEqSym` | `(x,y:Char) -> (x == y) = (y == x)` | **J** | Symmetry of `prim__eqChar`. Same reason as #1 — opaque primitive, no decision procedure to recurse on. **Externally validated** (same harness as #1). |
3030
| 3 | `SafetyLemmas.idr:218` | `unpackLength` | `length (unpack s) = length s` | **J ✓** | `unpack` = `prim__strToCharList` (foreign). `String` is opaque with no induction principle; the relation between primitive `String` length and `List Char` length is not reducible in-language. **Externally validated** via backend-assurance harness (`docs/backend-assurance/prim__strToCharList.md` + BEAM property test). |
31-
| 4 | `SafetyLemmas.idr:219` | `appendLengthSum` | `length (s ++ t) = length s + length t` | **J** | `++` on `String` = `prim__strAppend` (foreign). Length additivity is a backend-semantics guarantee, not type-level reducible. |
32-
| 5 | `SafetyLemmas.idr:226` | `substrLengthBound` | `LTE (length (substr start len s)) len` | **J** | `substr` = `prim__strSubstr` (foreign). The "result no longer than `len`" bound is a primitive-semantics guarantee with no in-language proof. |
31+
| 4 | `SafetyLemmas.idr:226` | `appendLengthSum` | `length (s ++ t) = length s + length t` | **J** | `++` on `String` = `prim__strAppend` (foreign). Length additivity is a backend-semantics guarantee, not type-level reducible. **Externally validated** via backend-assurance harness (`docs/backend-assurance/prim__strAppend.md` + BEAM property test). |
32+
| 5 | `SafetyLemmas.idr:233` | `substrLengthBound` | `LTE (length (substr start len s)) len` | **J** | `substr` = `prim__strSubstr` (foreign). The "result no longer than `len`" bound is a primitive-semantics guarantee with no in-language proof. **Externally validated** via backend-assurance harness (`docs/backend-assurance/prim__strSubstr.md` + BEAM property test). |
3333

3434
**Verdict: 5/5 are class (J).** All five reduce to the same root cause:
3535
Idris2 treats `Char` and `String` as opaque primitive types whose
@@ -39,6 +39,15 @@ principle. There is no constructive in-language proof for any of them; a
3939
short of changing the trusted computing base. They are correctly marked
4040
`%unsafe`, individually documented, and isolated in one module.
4141

42+
The **J ✓** marker indicates the axiom is class (J) **and**
43+
externally validated by the backend-assurance harness (see
44+
`docs/backend-assurance/`). The validation does not change the
45+
in-language proof — the `believe_me` sites stay in source. It shrinks
46+
the trusted base from "we trust the backend" to "we have read the
47+
backend lowering and randomly tested the operation against the
48+
claimed property". A bare **J** indicates a class-(J) axiom whose
49+
harness has not yet landed.
50+
4251
No **(R)** or **(S)** sites were found. The audit's "9" was a raw text
4352
count conflating 5 real axioms with 4 comment mentions.
4453

@@ -74,11 +83,11 @@ All five are class **(J)** — genuinely unavoidable in Idris2 0.8.0
7483

7584
| Axiom | Site | Justification | Backend-assurance evidence |
7685
|-------|------|---------------|----------------------------|
77-
| `charEqSound` | `SafetyLemmas.idr:53` | Soundness of `prim__eqChar` — backend primitive correctness | `docs/backend-assurance/prim__eqChar.md` + `elixir/test/backend_assurance/prim_eq_char_test.exs` |
78-
| `charEqSym` | `SafetyLemmas.idr:60` | Symmetry of `prim__eqChar` — backend primitive correctness | `docs/backend-assurance/prim__eqChar.md` + `elixir/test/backend_assurance/prim_eq_char_test.exs` |
86+
| `charEqSound` | `SafetyLemmas.idr:60` | Soundness of `prim__eqChar` — backend primitive correctness, externally validated | `docs/backend-assurance/prim__eqChar.md` + `elixir/test/backend_assurance/prim_eq_char_test.exs` |
87+
| `charEqSym` | `SafetyLemmas.idr:67` | Symmetry of `prim__eqChar` — backend primitive correctness, externally validated | `docs/backend-assurance/prim__eqChar.md` + `elixir/test/backend_assurance/prim_eq_char_test.exs` |
7988
| `unpackLength` | `SafetyLemmas.idr:218` | `prim__strToCharList` preserves length — backend primitive correctness, externally validated | `docs/backend-assurance/prim__strToCharList.md` + `elixir/test/backend_assurance/prim_str_to_char_list_test.exs` |
80-
| `appendLengthSum` | `SafetyLemmas.idr:219` | `prim__strAppend` length semantics — not reducible at type level | _pending_ |
81-
| `substrLengthBound` | `SafetyLemmas.idr:226` | `prim__strSubstr` length bound — not reducible at type level | _pending_ |
89+
| `appendLengthSum` | `SafetyLemmas.idr:226` | `prim__strAppend` length semantics — not reducible at type level, externally validated | `docs/backend-assurance/prim__strAppend.md` + `elixir/test/backend_assurance/prim_str_append_test.exs` |
90+
| `substrLengthBound` | `SafetyLemmas.idr:233` | `prim__strSubstr` length bound — not reducible at type level, externally validated | `docs/backend-assurance/prim__strSubstr.md` + `elixir/test/backend_assurance/prim_str_substr_test.exs` |
8291

8392
Note: `logSafeBounded` in SafeAPIKey.idr no longer uses `believe_me` directly;
8493
it calls the documented SafetyLemmas axioms via structural proof.
@@ -105,10 +114,11 @@ sites stay in `SafetyLemmas.idr`. The harness shrinks the trusted base
105114
from "we trust the backend" to "we read the lowering and randomly
106115
tested the operation".
107116

108-
First primitive landed: `prim__eqChar` (covering both `charEqSound`
109-
and `charEqSym`). Remaining three (`prim__strToCharList`,
110-
`prim__strAppend`, `prim__strSubstr`) tracked under epic #87 Tier C
111-
backend-assurance campaign — one PR per primitive.
117+
Primitives validated so far: `prim__eqChar` (covering `charEqSound`
118+
and `charEqSym`), `prim__strToCharList` (covering `unpackLength`),
119+
`prim__strAppend` (covering `appendLengthSum`), and `prim__strSubstr`
120+
(covering `substrLengthBound`). Tracked under epic #87 Tier C
121+
backend-assurance campaign.
112122

113123
## Priority Going Forward
114124

docs/backend-assurance/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ companion property-test harness lives under
2828
|--------------------|--------------------------------------|----------------------------------|----------------------------------------------------------------|
2929
| `prim__eqChar` | `charEqSound`, `charEqSym` | `prim__eqChar.md` | `elixir/test/backend_assurance/prim_eq_char_test.exs` |
3030
| `prim__strToCharList` | `unpackLength` | `prim__strToCharList.md` | `elixir/test/backend_assurance/prim_str_to_char_list_test.exs` |
31-
| `prim__strAppend` | `appendLengthSum` | _pending_ | _pending_ |
32-
| `prim__strSubstr` | `substrLengthBound` | _pending_ | _pending_ |
31+
| `prim__strAppend` | `appendLengthSum` | `prim__strAppend.md` | `elixir/test/backend_assurance/prim_str_append_test.exs` |
32+
| `prim__strSubstr` | `substrLengthBound` | `prim__strSubstr.md` | `elixir/test/backend_assurance/prim_str_substr_test.exs` |
3333

34-
Each row is delivered as one PR per primitive. `prim__eqChar` is the
35-
first; the other three follow the same shape.
34+
Each row is delivered as one PR per primitive.
3635

3736
## Constraints
3837

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Backend-Assurance: `prim__strAppend`
2+
3+
Trusted-extraction validation for the class-(J) axiom over Idris2's
4+
`prim__strAppend` primitive:
5+
6+
- `appendLengthSum : (s, t : String) -> length (s ++ t) = length s + length t`
7+
(`src/abi/Boj/SafetyLemmas.idr:226`)
8+
9+
Declared `%unsafe` with `believe_me ()` in Idris2 0.8.0 because
10+
`String` is an opaque primitive type with no constructors and no
11+
in-language induction principle. This document argues — by inspecting
12+
the backend lowerings that BoJ actually ships against — that the
13+
length-additivity property holds.
14+
15+
The companion property test
16+
(`elixir/test/backend_assurance/prim_str_append_test.exs`) exercises
17+
the BEAM half of this argument over the codepoint space.
18+
19+
## What `prim__strAppend` is
20+
21+
`prim__strAppend : String -> String -> String` is a primitive
22+
arithmetic operation declared in Idris2's `Core.Primitives`. The
23+
`++` operator on `String` is the `Semigroup String` instance, which
24+
is `prim__strAppend` directly. Length is `prim__strLength` (the
25+
`length : String -> Nat` definition in `Data.String`).
26+
27+
The question reduces to: does the operation `prim__strAppend` followed
28+
by `prim__strLength` satisfy `|prim__strAppend(s, t)| = |s| + |t|` on
29+
each shipping backend? Both `length` and `++` agree on a single notion
30+
of "character count" — what that notion is depends on the backend.
31+
32+
## Chez Scheme backend (Idris2 default codegen)
33+
34+
In `Compiler.Scheme.Chez`, `prim__strAppend` lowers to the R6RS
35+
procedure `string-append`, and `prim__strLength` lowers to
36+
`string-length`. On Chez 9.x:
37+
38+
- **String model.** R6RS §6.7 specifies that a Scheme string is a
39+
sequence of Unicode characters (codepoints), not a byte sequence.
40+
`string-length` returns the number of characters.
41+
- **`string-append` semantics.** R6RS §11.12 specifies `string-append`
42+
returns a newly-allocated string whose characters are the
43+
concatenation, in order, of the characters of the argument strings.
44+
- **Length additivity.** Combining the two: the number of characters
45+
in `(string-append s t)` equals the number of characters in `s`
46+
plus the number of characters in `t`. This is part of the Scheme
47+
standard, not an implementation detail.
48+
49+
No further evidence needed beyond citing the standard.
50+
51+
## BEAM backend (Erlang / Elixir, where BoJ runs)
52+
53+
BoJ's REST surface is Elixir on the BEAM. The Idris2 proofs are
54+
compile-time-only artefacts; the runtime strings flowing through the
55+
system are BEAM UTF-8 binaries. On BEAM:
56+
57+
- **String model.** Elixir strings are UTF-8 encoded binaries.
58+
Idris2's `length`-on-`String` semantics on Chez are codepoint
59+
count. Elixir's `String.length/1` counts grapheme clusters, so the
60+
BEAM-side harness measures codepoint count explicitly via
61+
`String.codepoints/1`.
62+
- **Concatenation lowering.** Elixir's `<>` on binaries is the
63+
built-in `bif erlang:'++'/2` for iolists, ultimately compiling to
64+
a byte-level binary append. UTF-8 is prefix-free: appending two
65+
valid UTF-8 byte sequences yields a valid UTF-8 byte sequence whose
66+
codepoint boundaries are exactly the boundaries of the operands.
67+
- **Length additivity.** Because UTF-8 is prefix-free, the codepoint
68+
boundaries of `s <> t` are exactly the boundaries of `s` followed
69+
by the boundaries of `t`. Hence the codepoint count of `s <> t`
70+
equals the codepoint count of `s` plus the codepoint count of `t`.
71+
72+
The property test exercises this over random strings sampled from the
73+
legal codepoint range (excluding surrogates), plus explicit boundary
74+
strings spanning all four UTF-8 encoding widths (1/2/3/4 bytes) and
75+
the empty-string identity case.
76+
77+
## Why this isn't circular
78+
79+
The harness does not call `prim__strAppend`. It calls Elixir `<>`
80+
directly on UTF-8 binaries and measures codepoint count explicitly.
81+
The argument is: *BEAM binary concatenation preserves the exact
82+
UTF-8 codepoint sequence of both operands*, so demonstrating that the
83+
resulting codepoint count is additive validates the backend operation
84+
at the semantic level the axiom uses. The trusted-extraction step is
85+
reading the lowering; the property-test step is verifying the
86+
operation behaves as the lowering claims.
87+
88+
For Chez, we do not run a Scheme harness — R6RS is sufficient
89+
documentary evidence. If BoJ ever ships a backend whose string model
90+
is not Unicode codepoints (e.g. a byte-oriented C backend without
91+
UTF-8 awareness), this document gets a new section and a matching
92+
property test, **and the axiom may need to be restated in terms of
93+
byte length** — see the *Honest framing* clause in
94+
`docs/backend-assurance/README.md`.
95+
96+
## Edge cases considered
97+
98+
- **Empty strings.** `s <> "" = s` and `"" <> s = s` are tested
99+
explicitly. Length additivity reduces to `|s| + 0 = |s|` and
100+
`0 + |s| = |s|` respectively — corner cases of the main property
101+
but worth pinning to catch a backend that allocates a sentinel byte
102+
on empty append.
103+
- **Multi-byte codepoints.** Tested via boundary strings covering
104+
all four UTF-8 widths: ASCII (1 byte), Latin-1 supplement (2 bytes,
105+
e.g. `café`), CJK (3 bytes, e.g. `日本語`), and astral plane
106+
(4 bytes, e.g. `🦀`). Each width has a different number of bytes
107+
per codepoint, but the codepoint count is invariant.
108+
- **Surrogates** (`0xD800..0xDFFF`): excluded from the codepoint
109+
generator. These are illegal as standalone codepoints in
110+
well-formed Unicode; their presence would indicate a system-under-
111+
test bug, not a `prim__strAppend` failure.
112+
- **Normalisation.** Out of scope. `prim__strAppend` is byte-level
113+
(per UTF-8 prefix-free property) and does not compose canonical
114+
decompositions. A grapheme that is `e` + combining acute (two
115+
codepoints) appended to nothing remains two codepoints, regardless
116+
of whether the precomposed `é` (one codepoint) would canonically
117+
equal it.
118+
- **Three-way associativity.** Not in the axiom but cheap to assert.
119+
Length of `(s <> t) <> u` equals length of `s <> (t <> u)` equals
120+
`|s| + |t| + |u|`. Catches a backend whose concatenation
121+
associates differently on length than on byte order.
122+
123+
## References
124+
125+
- Idris2 0.8.0 `src/Core/Primitives.idr` — primitive operation table.
126+
- Idris2 0.8.0 `src/Compiler/Scheme/Chez.idr` — Chez codegen lowerings
127+
for `prim__strAppend` and `prim__strLength`.
128+
- R6RS §6.7, §11.12 — Scheme string model and `string-append`
129+
specification.
130+
- Elixir `String` module documentation — UTF-8 codepoint enumeration
131+
via `String.codepoints/1`.
132+
- `PROOF-NEEDS.md` — axiom audit (2026-05-18) and class-(J) framing.
133+
- `src/abi/Boj/SafetyLemmas.idr` — axiom declaration (line 226).

0 commit comments

Comments
 (0)