Trusted-extraction validation for the class-(J) axiom over Idris2's
prim__strSubstr primitive:
substrLengthBound : (s : String) -> (start, len : Nat) -> LTE (length (substr start len s)) len(src/abi/Boj/SafetyLemmas.idr:233)
Declared %unsafe with believe_me () in Idris2 0.8.0 because
String is an opaque primitive type. This document argues — by
inspecting the backend lowerings that BoJ actually ships against —
that the length-bound property holds.
The companion property test
(elixir/test/backend_assurance/prim_str_substr_test.exs) exercises
the BEAM half of this argument over the codepoint space.
prim__strSubstr : Int -> Int -> String -> String is a primitive
arithmetic operation declared in Idris2's Core.Primitives. The
substr : Nat -> Nat -> String -> String definition in
Data.String wraps the primitive after casting the Nat arguments
to Int. Length is prim__strLength (length : String -> Nat).
The question reduces to: does prim__strSubstr(start, len, s)
produce a string whose prim__strLength is at most len, for every
(start, len, s)?
A weaker phrasing — that the result has length equal to len
when start + len ≤ length(s), and at most len otherwise — is the
operational specification. The axiom only claims the upper bound,
which is the safety-relevant half (preventing buffer-length under-
estimation in downstream proofs).
In Compiler.Scheme.Chez, prim__strSubstr lowers to a Scheme
expression equivalent to (substring s start (min (+ start len) (string-length s))). The implementation clamps the end index to the
string's actual length so the call is total even when
start + len > length(s).
On Chez 9.x:
substringsemantics. R6RS §11.12 specifiessubstringreturns a newly-allocated string containing the characters of the argument fromstart(inclusive) toend(exclusive). Whenstart = end, the result is the empty string.- Length of the result. The number of characters in
(substring s start end)isend - start. With the clampend = min(start + len, string-length s):- If
start + len ≤ string-length s: result length is exactlylen. ✓LTE len len. - If
start ≥ string-length s: clamp forcesend = start, result is empty. ✓LTE 0 len. - Otherwise: result length is
string-length s - start, which by the clamp is< len. ✓.
- If
The bound result length ≤ len holds in all cases. This is part of
the R6RS guarantee for substring combined with the codegen's clamp.
BoJ's REST surface is Elixir on the BEAM. The runtime strings are
UTF-8 encoded binaries; the BEAM-side harness models substring over
the decoded codepoint sequence (String.to_charlist/1 + Enum.slice/3
to_string/1).
On BEAM:
- String model. As with
prim__strAppend, strings are UTF-8 binaries. Thelengthreferred to in the axiom is codepoint count, matching Idris2 semantics on Chez. Elixir'sString.length/1andString.slice/3are grapheme-oriented, so the BEAM-side harness uses explicit codepoint operations. - Codepoint-slice semantics. The harness decodes
swithString.to_charlist/1, slices that codepoint list withEnum.slice(start, len), and re-encodes it withto_string/1. This returns at mostlencodepoints froms, starting at codepoint offsetstart. The slice clamps to the string's actual codepoint length: whenstart ≥ length(s), the result is""; whenstart + len > length(s), the result is the suffix fromstartto the end (which is strictly shorter thanlen). - Length bound. Combining the two facts: the codepoint count of
the codepoint slice is
≤ lenfor all(start, len, s). The clamp can only shorten the result; it never extends pastlen.
The property test exercises this over random (start, len, s) tuples
plus explicit boundary cases: len = 0, start ≥ length(s),
start = 0 and len = length(s), and multi-byte codepoint strings
where codepoint count differs from byte count.
The harness does not call prim__strSubstr. It calls Elixir
charlist/codepoint operations directly. The argument is: a
codepoint-level substring operation over a UTF-8 binary clamps to at
most the requested number of codepoints, so demonstrating that
operation satisfies the property validates the backend semantics the
axiom uses. The trusted-extraction step is reading the lowering; the
property-test step is verifying the operation behaves as the lowering
claims.
For Chez, we do not run a Scheme harness — the R6RS substring
semantics combined with the codegen's clamp are sufficient
documentary evidence.
len = 0. The tight corner of the bound: result must be the empty string. R6RSsubstringwithend = startreturns""; the BEAM codepoint slice withlen = 0returns"". Both satisfyLTE 0 0.start ≥ length(s)(start past end). Both backends clamp to the empty string; the boundLTE 0 lenis trivial.start + len > length(s)(overflow tail). Both backends clamp the result to the suffix fromstartto the actual end. The resulting length islength(s) - start, which is strictly less thanlen(sincelength(s) < start + len).start = 0,len = length(s)(whole-string slice). The result issitself; the bound is tight (LTE length(s) length(s)).- Multi-byte codepoints. Tested via boundary strings covering
ASCII, Latin-1 supplement, CJK, and astral plane. The harness slices
codepoints, not bytes, so a slice of length
lenfrom an emoji-heavy string spans up to4 * lenbytes but at mostlencodepoints. - Surrogates (
0xD800..0xDFFF): excluded from the codepoint generator. Same rationale asprim__strAppend. - Negative
start/len. Idris2'ssubstrtakesNat, so the values are non-negative by typing. The BEAM-side test generators sample from0..96only; the harness does not assert behaviour on negative inputs because the type system rules them out upstream.
- Idris2 0.8.0
src/Core/Primitives.idr— primitive operation table. - Idris2 0.8.0
src/Compiler/Scheme/Chez.idr— Chez codegen forprim__strSubstr(clamp + R6RSsubstring). - R6RS §11.12 —
substringspecification. - Elixir
Stringmodule documentation —String.to_charlist/1semantics over UTF-8 binaries. PROOF-NEEDS.md— axiom audit (2026-05-18) and class-(J) framing.src/abi/Boj/SafetyLemmas.idr— axiom declaration (line 233).