Skip to content

Commit 3305c92

Browse files
proof(xml-7-44): make Xml744.Escape.exfiltrate provably total (#79)
## Summary - Resolves the three `assert_total` proof escapes in `Xml744.Escape.exfiltrate`'s inner `replaceAll` worker (originally wrapping partial calls to `strHead`, `head`, and `tail` on Idris2 0.7). - The earlier refactor that pattern-matched the partial calls away **left the function failing Idris2's termination check** on 0.8.0 — recursion through `pack $ drop ...` is not structurally smaller, so the file did not actually build (`exfiltrate is not total, possibly not terminating`). - This PR rewrites the worker as `replaceAllChars : Nat -> List Char -> List Char -> List Char -> List Char` with a fuel parameter sized to `length (unpack str)`. Each recursive step consumes ≥ 1 char of the third argument so the fuel bound is never the binding constraint — it just makes termination structurally obvious. Matches the Nat-fueled pattern from the ephapax totality campaign (standards#134). ## Verification ``` $ idris2 -p contrib Xml744/Escape.idr 1/1: Building Xml744.Escape (Xml744/Escape.idr) Xml744.Escape> :total Xml744.Escape.exfiltrate Xml744.Escape.exfiltrate is total Xml744.Escape> :total Xml744.Escape.infiltrate Xml744.Escape.infiltrate is total Xml744.Escape> :total Xml744.Escape.infiltrateAttr Xml744.Escape.infiltrateAttr is total Xml744.Escape> :total Xml744.Escape.makeSafe Xml744.Escape.makeSafe is total ``` `grep -E 'assert_total|assert_smaller|believe_me|%unsafe|partial ' src/Xml744/Escape.idr` matches only the new doc-comment line referencing what is *not* used. ## Out of scope (separate proof debts in this toolkit) - `Xml744.Attribute` has a pre-existing baseline issue (`decEq` not in scope for `Bool` on Idris2 0.8.0) and a `PROOF_TODO`-marked `cast (MkAttrName s)` in `unsafeAttrName` — both predate this branch and are distinct from the three escapes called out in the proof-debt request. ## Test plan - [ ] CI green - [ ] `idris2 --build idris2-ecosystem/xml-toolkit/xml-7-44.ipkg` builds `Xml744.Escape` cleanly (verified locally; downstream module errors are unrelated and tracked separately) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3ed5c3c commit 3305c92

1 file changed

Lines changed: 16 additions & 10 deletions

File tree

idris2-ecosystem/xml-toolkit/src/Xml744/Escape.idr

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ infiltrateAttr : String -> String
4848
infiltrateAttr = concatMap escapeAttrChar . unpack
4949

5050
||| Exfiltrate: safely extract content from XML (unescape entities)
51+
|||
52+
||| The worker `replaceAllChars` recurses on a `Nat` fuel sized to the input.
53+
||| Each step consumes at least one character of the third argument, so the
54+
||| fuel bound is never reached in practice — it exists to make termination
55+
||| structurally obvious to Idris2 without `assert_total` / `assert_smaller`.
5156
export
5257
exfiltrate : String -> String
5358
exfiltrate s =
@@ -58,18 +63,19 @@ exfiltrate s =
5863
s5 = replaceAll "&apos;" "'" s4
5964
in s5
6065
where
66+
replaceAllChars : Nat -> List Char -> List Char -> List Char -> List Char
67+
replaceAllChars _ _ _ [] = []
68+
replaceAllChars _ [] _ xs = xs
69+
replaceAllChars Z _ _ xs = xs
70+
replaceAllChars (S k) (f :: fs) to (x :: xs) =
71+
if isPrefixOf (f :: fs) (x :: xs)
72+
then to ++ replaceAllChars k (f :: fs) to (drop (length fs) xs)
73+
else x :: replaceAllChars k (f :: fs) to xs
74+
6175
replaceAll : String -> String -> String -> String
6276
replaceAll from to str =
63-
case unpack from of
64-
[] => str -- empty search string, return unchanged
65-
(fc :: _) =>
66-
case break (== fc) (unpack str) of
67-
(before, []) => str
68-
(before, (r :: rs)) =>
69-
if isPrefixOf (unpack from) (r :: rs)
70-
then pack before ++ to ++ replaceAll from to (pack $ drop (length from) (r :: rs))
71-
else pack before ++ singleton r ++
72-
replaceAll from to (pack rs)
77+
let chars = unpack str
78+
in pack (replaceAllChars (length chars) (unpack from) (unpack to) chars)
7379

7480
||| Check if a string contains any unescaped dangerous characters
7581
export

0 commit comments

Comments
 (0)