From 79659457fb0377da019c274eb58e5de2ff95d9d0 Mon Sep 17 00:00:00 2001 From: Julia Markus Himmel <2065352+TwoFX@users.noreply.github.com> Date: Thu, 7 May 2026 16:27:06 +0200 Subject: [PATCH] chore: reduce usage of `String.length` (#13681) This PR reduces the usage of `String.length` in our codebase. This is just the first step of many towards eliminating `String.length`. --- src/Init/Data/Nat/ToString.lean | 1 + src/Init/Data/String.lean | 1 + src/Init/Data/String/Basic.lean | 55 +-------------- src/Init/Data/String/Extra.lean | 1 + src/Init/Data/String/Lemmas.lean | 1 + src/Init/Data/String/Lemmas/Intercalate.lean | 5 -- src/Init/Data/String/Lemmas/IsEmpty.lean | 4 -- src/Init/Data/String/Lemmas/Length.lean | 31 +++++++++ src/Init/Data/String/Lemmas/Modify.lean | 4 -- .../Data/String/Lemmas/Pattern/Basic.lean | 8 +-- src/Init/Data/String/Lemmas/Pattern/Char.lean | 12 ++-- src/Init/Data/String/Lemmas/Pattern/Pred.lean | 4 +- .../String/Lemmas/Pattern/String/Basic.lean | 8 +-- .../String/Lemmas/Pattern/TakeDrop/Char.lean | 4 +- src/Init/Data/String/Lemmas/Splits.lean | 12 ++-- src/Init/Data/String/Lemmas/TakeDrop.lean | 8 +-- src/Init/Data/String/Length.lean | 68 +++++++++++++++++++ src/Init/Data/String/TakeDrop.lean | 8 +-- src/Init/System/FilePath.lean | 1 + src/Init/System/IO.lean | 4 +- src/Init/System/Uri.lean | 1 + src/Lean/Data/EditDistance.lean | 1 + src/Lean/Data/Name.lean | 1 + src/Lean/DocString/Links.lean | 47 ++++++------- src/Lean/DocString/Markdown.lean | 1 + src/Lean/Message.lean | 5 +- src/Lean/Meta/MethodSpecs.lean | 2 +- src/Lean/Util/Path.lean | 1 + src/Lean/Widget/TaggedText.lean | 1 + src/Std/Http/Data/URI/Basic.lean | 1 + src/Std/Internal/Parsec/String.lean | 1 + src/Std/Time/Duration.lean | 1 + src/Std/Time/Format/Basic.lean | 8 +-- src/lake/Lake/Util/Cli.lean | 1 + src/lake/Lake/Util/String.lean | 1 + src/lake/Lake/Util/Version.lean | 1 + 36 files changed, 180 insertions(+), 134 deletions(-) create mode 100644 src/Init/Data/String/Lemmas/Length.lean create mode 100644 src/Init/Data/String/Length.lean diff --git a/src/Init/Data/Nat/ToString.lean b/src/Init/Data/Nat/ToString.lean index 00dc083226..c8264d0a2a 100644 --- a/src/Init/Data/Nat/ToString.lean +++ b/src/Init/Data/Nat/ToString.lean @@ -10,6 +10,7 @@ public import Init.Data.Repr public import Init.Data.Char.Basic public import Init.Data.ToString.Basic public import Init.Data.String.Basic +public import Init.Data.String.Length import Init.NotationExtra import all Init.Data.Repr import Init.Omega diff --git a/src/Init/Data/String.lean b/src/Init/Data/String.lean index d859797e13..70263d9230 100644 --- a/src/Init/Data/String.lean +++ b/src/Init/Data/String.lean @@ -31,3 +31,4 @@ public import Init.Data.String.Subslice public import Init.Data.String.Iter public import Init.Data.String.Iterate public import Init.Data.String.Hashable +public import Init.Data.String.Length diff --git a/src/Init/Data/String/Basic.lean b/src/Init/Data/String/Basic.lean index d57fc02395..7ff696d6c2 100644 --- a/src/Init/Data/String/Basic.lean +++ b/src/Init/Data/String/Basic.lean @@ -264,28 +264,6 @@ theorem String.toList_empty : "".toList = [] := by theorem String.data_empty : "".toList = [] := toList_empty -/-- -Returns the length of a string in Unicode code points. - -Examples: -* `"".length = 0` -* `"abc".length = 3` -* `"L∃∀N".length = 4` --/ -@[extern "lean_string_length", expose, tagged_return] -def String.length (b : @& String) : Nat := - b.toList.length - -@[simp] -theorem String.Internal.size_toArray {b : String} : (String.Internal.toArray b).size = b.length := - (rfl) - -@[simp] -theorem String.length_toList {s : String} : s.toList.length = s.length := (rfl) - -@[deprecated String.length_toList (since := "2025-10-30")] -theorem String.length_data {b : String} : b.toList.length = b.length := (rfl) - private theorem ByteArray.utf8Decode?go_eq_utf8Decode?go_extract {b : ByteArray} {hi : i ≤ b.size} {acc : Array Char} : utf8Decode?.go b i acc hi = (utf8Decode?.go (b.extract i b.size) 0 #[] (by simp)).map (acc ++ ·) := by fun_cases utf8Decode?.go b i acc hi with @@ -436,14 +414,6 @@ theorem String.ofList_eq_empty_iff {l : List Char} : String.ofList l = "" ↔ l theorem List.asString_eq_empty_iff {l : List Char} : String.ofList l = "" ↔ l = [] := String.ofList_eq_empty_iff -@[simp] -theorem String.length_ofList {l : List Char} : (String.ofList l).length = l.length := by - rw [← String.length_toList, String.toList_ofList] - -@[deprecated String.length_ofList (since := "2025-10-30")] -theorem List.length_asString {l : List Char} : (String.ofList l).length = l.length := - String.length_ofList - end namespace String @@ -531,7 +501,7 @@ theorem Pos.Raw.isValid_ofList {l : List Char} {p : Pos.Raw} : rw [isValid_iff_exists_append] refine ⟨?_, ?_⟩ · rintro ⟨t₁, t₂, ht, rfl⟩ - refine ⟨t₁.length, ?_⟩ + refine ⟨t₁.toList.length, ?_⟩ have := congrArg String.toList ht simp only [String.toList_ofList, String.toList_append] at this simp [this] @@ -3127,8 +3097,6 @@ namespace String theorem ext {s₁ s₂ : String} (h : s₁.toList = s₂.toList) : s₁ = s₂ := toList_injective h -@[simp] theorem length_empty : "".length = 0 := by simp [← length_toList, toList_empty] - @[deprecated singleton_eq_ofList (since := "2025-10-30")] theorem singleton_eq {c : Char} : String.singleton c = ofList [c] := singleton_eq_ofList @@ -3140,10 +3108,6 @@ theorem singleton_eq {c : Char} : String.singleton c = ofList [c] := theorem data_singleton (c : Char) : (String.singleton c).toList = [c] := toList_singleton c -@[simp] -theorem length_singleton {c : Char} : (String.singleton c).length = 1 := by - simp [← length_toList] - @[simp] theorem toList_push (c : Char) : (String.push s c).toList = s.toList ++ [c] := by simp [← append_singleton] @@ -3152,15 +3116,6 @@ theorem toList_push (c : Char) : (String.push s c).toList = s.toList ++ [c] := b theorem data_push (c : Char) : (String.push s c).toList = s.toList ++ [c] := toList_push c -@[simp] theorem length_push (c : Char) : (String.push s c).length = s.length + 1 := by - simp [← length_toList] - -@[simp] theorem length_pushn (c : Char) (n : Nat) : (pushn s c n).length = s.length + n := by - rw [pushn_eq_repeat_push]; induction n <;> simp [Nat.repeat, Nat.add_assoc, *] - -@[simp] theorem length_append (s t : String) : (s ++ t).length = s.length + t.length := by - simp [← length_toList] - theorem lt_iff {s t : String} : s < t ↔ s.toList < t.toList := .rfl namespace Pos.Raw @@ -3211,11 +3166,3 @@ theorem bytes_inj {s t : String} : s.toByteArray = t.toByteArray ↔ s = t := toByteArray_inj end String - -namespace Char - -@[deprecated String.length_singleton (since := "2026-02-12")] -theorem length_toString (c : Char) : c.toString.length = 1 := by - simp - -end Char diff --git a/src/Init/Data/String/Extra.lean b/src/Init/Data/String/Extra.lean index efd475a871..2489996230 100644 --- a/src/Init/Data/String/Extra.lean +++ b/src/Init/Data/String/Extra.lean @@ -11,6 +11,7 @@ public import Init.Data.String.Basic import all Init.Data.String.Basic import Init.Data.String.Search import Init.Data.String.Termination +import Init.Data.String.Length public section diff --git a/src/Init/Data/String/Lemmas.lean b/src/Init/Data/String/Lemmas.lean index 93bf092505..7f82405d66 100644 --- a/src/Init/Data/String/Lemmas.lean +++ b/src/Init/Data/String/Lemmas.lean @@ -21,3 +21,4 @@ public import Init.Data.String.Lemmas.Iter public import Init.Data.String.Lemmas.Hashable public import Init.Data.String.Lemmas.TakeDrop public import Init.Data.String.Lemmas.StringOrder +public import Init.Data.String.Lemmas.Length diff --git a/src/Init/Data/String/Lemmas/Intercalate.lean b/src/Init/Data/String/Lemmas/Intercalate.lean index 44e6ed80fb..a3f7796990 100644 --- a/src/Init/Data/String/Lemmas/Intercalate.lean +++ b/src/Init/Data/String/Lemmas/Intercalate.lean @@ -81,11 +81,6 @@ theorem toList_join {l : List String} : (String.join l).toList = l.flatMap Strin theorem join_append {l m : List String} : String.join (l ++ m) = String.join l ++ String.join m := by simp [← toList_inj] -@[simp] -theorem length_join {l : List String} : (String.join l).length = (l.map String.length).sum := by - simp only [← length_toList, toList_join, List.length_flatMap] - simp - namespace Slice @[simp] diff --git a/src/Init/Data/String/Lemmas/IsEmpty.lean b/src/Init/Data/String/Lemmas/IsEmpty.lean index 1c92a169d0..4fc4909fa4 100644 --- a/src/Init/Data/String/Lemmas/IsEmpty.lean +++ b/src/Init/Data/String/Lemmas/IsEmpty.lean @@ -162,10 +162,6 @@ theorem Slice.isEmpty_slice_eq_false_iff {s : Slice} {p₁ p₂ h} : (s.slice p₁ p₂ h).isEmpty = false ↔ p₁ ≠ p₂ := by rw [ne_eq, ← isEmpty_slice (h := h), Bool.not_eq_true] -@[simp] -theorem length_eq_zero_iff {s : String} : s.length = 0 ↔ s = "" := by - simp [← length_toList] - @[simp] theorem toByteArray_eq_empty_iff {s : String} : s.toByteArray = ByteArray.empty ↔ s = "" := by diff --git a/src/Init/Data/String/Lemmas/Length.lean b/src/Init/Data/String/Lemmas/Length.lean new file mode 100644 index 0000000000..81649fbb6c --- /dev/null +++ b/src/Init/Data/String/Lemmas/Length.lean @@ -0,0 +1,31 @@ +/- +Copyright (c) 2026 Lean FRO, LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Markus Himmel +-/ +module + +prelude +public import Init.Data.String.Length +public import Init.Data.String.Modify +import Init.Data.String.Lemmas.Modify +import Init.Data.String.Lemmas.Intercalate + +public section + +namespace String + +@[simp] +theorem length_eq_zero_iff {s : String} : s.length = 0 ↔ s = "" := by + simp [← length_toList] + +@[simp] +theorem length_map {f : Char → Char} {s : String} : (s.map f).length = s.length := by + simp [← length_toList] + +@[simp] +theorem length_join {l : List String} : (String.join l).length = (l.map String.length).sum := by + simp only [← length_toList, toList_join, List.length_flatMap] + simp [length_toList] + +end String diff --git a/src/Init/Data/String/Lemmas/Modify.lean b/src/Init/Data/String/Lemmas/Modify.lean index 5818a191d4..be3926c275 100644 --- a/src/Init/Data/String/Lemmas/Modify.lean +++ b/src/Init/Data/String/Lemmas/Modify.lean @@ -57,10 +57,6 @@ theorem map_eq_internal {f : Char → Char} {s : String} : s.map f = .ofList (s. apply String.toList_injective simp only [toList_map, toList_ofList] -@[simp] -theorem length_map {f : Char → Char} {s : String} : (s.map f).length = s.length := by - simp [← length_toList] - @[simp] theorem map_eq_empty {f : Char → Char} {s : String} : s.map f = "" ↔ s = "" := by simp [← toList_eq_nil_iff] diff --git a/src/Init/Data/String/Lemmas/Pattern/Basic.lean b/src/Init/Data/String/Lemmas/Pattern/Basic.lean index 956269e15f..738b9eb2c9 100644 --- a/src/Init/Data/String/Lemmas/Pattern/Basic.lean +++ b/src/Init/Data/String/Lemmas/Pattern/Basic.lean @@ -301,8 +301,8 @@ This implies that the notion of match and longest match coincide. class NoPrefixPatternModel {ρ : Type} (pat : ρ) [PatternModel pat] : Prop where eq_empty (s t) : PatternModel.Matches pat s → PatternModel.Matches pat (s ++ t) → t = "" -theorem NoPrefixPatternModel.of_length_eq {ρ : Type} {pat : ρ} [PatternModel pat] - (h : ∀ s t, PatternModel.Matches pat s → PatternModel.Matches pat t → s.length = t.length) : +theorem NoPrefixPatternModel.of_length_toList_eq {ρ : Type} {pat : ρ} [PatternModel pat] + (h : ∀ s t, PatternModel.Matches pat s → PatternModel.Matches pat t → s.toList.length = t.toList.length) : NoPrefixPatternModel pat where eq_empty s t hs ht := by simpa using h s _ hs ht @@ -322,8 +322,8 @@ This implies that the notion of reverse match and longest reverse match coincide class NoSuffixPatternModel {ρ : Type} (pat : ρ) [PatternModel pat] : Prop where eq_empty (s t) : PatternModel.Matches pat t → PatternModel.Matches pat (s ++ t) → s = "" -theorem NoSuffixPatternModel.of_length_eq {ρ : Type} {pat : ρ} [PatternModel pat] - (h : ∀ s t, PatternModel.Matches pat s → PatternModel.Matches pat t → s.length = t.length) : +theorem NoSuffixPatternModel.of_length_toList_eq {ρ : Type} {pat : ρ} [PatternModel pat] + (h : ∀ s t, PatternModel.Matches pat s → PatternModel.Matches pat t → s.toList.length = t.toList.length) : NoSuffixPatternModel pat where eq_empty s t hs ht := by simpa using h t _ hs ht diff --git a/src/Init/Data/String/Lemmas/Pattern/Char.lean b/src/Init/Data/String/Lemmas/Pattern/Char.lean index 5edab67ce5..c556173ec0 100644 --- a/src/Init/Data/String/Lemmas/Pattern/Char.lean +++ b/src/Init/Data/String/Lemmas/Pattern/Char.lean @@ -33,10 +33,10 @@ instance {c : Char} : StrictPatternModel c where not_matches_empty := by simp [PatternModel.Matches] instance {c : Char} : NoPrefixPatternModel c := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) instance {c : Char} : NoSuffixPatternModel c := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) theorem isMatch_iff {c : Char} {s : Slice} {pos : s.Pos} : IsMatch c pos ↔ @@ -186,12 +186,12 @@ theorem isLongestMatchAtChain_iff {c : Char} {s : Slice} {pos pos' : s.Pos} : theorem isLongestMatchAtChain_iff_toList {c : Char} {s : Slice} {pos pos' : s.Pos} : IsLongestMatchAtChain c pos pos' ↔ - ∃ (h : pos ≤ pos'), (s.slice pos pos' h).copy.toList = List.replicate (s.slice pos pos' h).copy.length c := by + ∃ (h : pos ≤ pos'), (s.slice pos pos' h).copy.toList = List.replicate (s.slice pos pos' h).copy.toList.length c := by simp [isLongestMatchAtChain_iff_isLongestMatchAtChain_beq, CharPred.isLongestMatchAtChain_iff_toList, List.eq_replicate_iff] theorem isLongestMatchAtChain_startPos_endPos_iff_toList {c : Char} {s : Slice} : - IsLongestMatchAtChain c s.startPos s.endPos ↔ s.copy.toList = List.replicate s.copy.length c := by + IsLongestMatchAtChain c s.startPos s.endPos ↔ s.copy.toList = List.replicate s.copy.toList.length c := by simp [isLongestMatchAtChain_iff_isLongestMatchAtChain_beq, CharPred.isLongestMatchAtChain_startPos_endPos_iff_toList, List.eq_replicate_iff] @@ -216,12 +216,12 @@ theorem isLongestRevMatchAtChain_iff {c : Char} {s : Slice} {pos pos' : s.Pos} : theorem isLongestRevMatchAtChain_iff_toList {c : Char} {s : Slice} {pos pos' : s.Pos} : IsLongestRevMatchAtChain c pos pos' ↔ - ∃ (h : pos ≤ pos'), (s.slice pos pos' h).copy.toList = List.replicate (s.slice pos pos' h).copy.length c := by + ∃ (h : pos ≤ pos'), (s.slice pos pos' h).copy.toList = List.replicate (s.slice pos pos' h).copy.toList.length c := by simp [isLongestRevMatchAtChain_iff_isLongestRevMatchAtChain_beq, CharPred.isLongestRevMatchAtChain_iff_toList, List.eq_replicate_iff] theorem isLongestRevMatchAtChain_startPos_endPos_iff_toList {c : Char} {s : Slice} : - IsLongestRevMatchAtChain c s.startPos s.endPos ↔ s.copy.toList = List.replicate s.copy.length c := by + IsLongestRevMatchAtChain c s.startPos s.endPos ↔ s.copy.toList = List.replicate s.copy.toList.length c := by simp [isLongestRevMatchAtChain_iff_isLongestRevMatchAtChain_beq, CharPred.isLongestRevMatchAtChain_startPos_endPos_iff_toList, List.eq_replicate_iff] diff --git a/src/Init/Data/String/Lemmas/Pattern/Pred.lean b/src/Init/Data/String/Lemmas/Pattern/Pred.lean index d36ae40079..949c364d30 100644 --- a/src/Init/Data/String/Lemmas/Pattern/Pred.lean +++ b/src/Init/Data/String/Lemmas/Pattern/Pred.lean @@ -33,10 +33,10 @@ instance {p : Char → Bool} : StrictPatternModel p where not_matches_empty := by simp [PatternModel.Matches] instance {p : Char → Bool} : NoPrefixPatternModel p := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) instance {p : Char → Bool} : NoSuffixPatternModel p := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) theorem isMatch_iff {p : Char → Bool} {s : Slice} {pos : s.Pos} : IsMatch p pos ↔ diff --git a/src/Init/Data/String/Lemmas/Pattern/String/Basic.lean b/src/Init/Data/String/Lemmas/Pattern/String/Basic.lean index 7cad6d2d3e..1861c24406 100644 --- a/src/Init/Data/String/Lemmas/Pattern/String/Basic.lean +++ b/src/Init/Data/String/Lemmas/Pattern/String/Basic.lean @@ -29,10 +29,10 @@ theorem strictPatternModel {pat : Slice} (hpat : pat.isEmpty = false) : StrictPa not_matches_empty := by simpa [PatternModel.Matches] instance {pat : Slice} : NoPrefixPatternModel pat := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) instance {pat : Slice} : NoSuffixPatternModel pat := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) theorem isMatch_iff {pat s : Slice} {pos : s.Pos} : IsMatch pat pos ↔ (s.sliceTo pos).copy = pat.copy := by @@ -287,10 +287,10 @@ theorem strictPatternModel {pat : String} (h : pat ≠ "") : StrictPatternModel not_matches_empty := by simpa [PatternModel.Matches] instance {pat : String} : NoPrefixPatternModel pat := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) instance {pat : String} : NoSuffixPatternModel pat := - .of_length_eq (by simp +contextual [PatternModel.Matches]) + .of_length_toList_eq (by simp +contextual [PatternModel.Matches]) theorem isMatch_iff_slice {pat : String} {s : Slice} {pos : s.Pos} : IsMatch (ρ := String) pat pos ↔ IsMatch (ρ := Slice) pat.toSlice pos := by diff --git a/src/Init/Data/String/Lemmas/Pattern/TakeDrop/Char.lean b/src/Init/Data/String/Lemmas/Pattern/TakeDrop/Char.lean index 9b8163fd8c..dda11b480c 100644 --- a/src/Init/Data/String/Lemmas/Pattern/TakeDrop/Char.lean +++ b/src/Init/Data/String/Lemmas/Pattern/TakeDrop/Char.lean @@ -87,7 +87,7 @@ theorem get_eq_of_lt_skipPrefixWhile_char {c : Char} {s : Slice} {pos : s.Pos} ( Pos.get_eq_of_lt_skipWhile_char (Pos.startPos_le _) (by rwa [skipPrefixWhile_eq_skipWhile_startPos] at h) @[simp] -theorem all_char_iff {c : Char} {s : Slice} : s.all c ↔ s.copy.toList = List.replicate s.copy.length c := by +theorem all_char_iff {c : Char} {s : Slice} : s.all c ↔ s.copy.toList = List.replicate s.copy.toList.length c := by rw [Bool.eq_iff_iff] simp [Pattern.Model.all_eq_true_iff, Char.isLongestMatchAtChain_startPos_endPos_iff_toList] @@ -123,7 +123,7 @@ theorem get_eq_of_skipSuffixWhile_le_char {c : Char} {s : Slice} {pos : s.Pos} Pos.get_eq_of_revSkipWhile_le_char h' (by rwa [skipSuffixWhile_eq_revSkipWhile_endPos] at h) @[simp] -theorem revAll_char_iff {c : Char} {s : Slice} : s.revAll c ↔ s.copy.toList = List.replicate s.copy.length c := by +theorem revAll_char_iff {c : Char} {s : Slice} : s.revAll c ↔ s.copy.toList = List.replicate s.copy.toList.length c := by rw [Bool.eq_iff_iff] simp [Pattern.Model.revAll_eq_true_iff, Char.isLongestRevMatchAtChain_startPos_endPos_iff_toList] diff --git a/src/Init/Data/String/Lemmas/Splits.lean b/src/Init/Data/String/Lemmas/Splits.lean index eda457a235..c9ef153a9a 100644 --- a/src/Init/Data/String/Lemmas/Splits.lean +++ b/src/Init/Data/String/Lemmas/Splits.lean @@ -760,9 +760,9 @@ theorem splits_nextn_startPos (s : String) (n : Nat) : simpa using s.splits_startPos.nextn n theorem Slice.Pos.Splits.prevn {s : Slice} {t₁ t₂ : String} {p : s.Pos} (h : p.Splits t₁ t₂) (n : Nat) : - (p.prevn n).Splits (String.ofList (t₁.toList.take (t₁.length - n))) (String.ofList (t₁.toList.drop (t₁.length - n)) ++ t₂) := by + (p.prevn n).Splits (String.ofList (t₁.toList.take (t₁.toList.length - n))) (String.ofList (t₁.toList.drop (t₁.toList.length - n)) ++ t₂) := by induction n generalizing p t₁ t₂ with - | zero => simpa [← String.length_toList] + | zero => simpa | succ n ih => rw [Pos.prevn_add_one] split @@ -771,16 +771,16 @@ theorem Slice.Pos.Splits.prevn {s : Slice} {t₁ t₂ : String} {p : s.Pos} (h : simpa [Nat.add_sub_add_right, List.take_append, List.drop_append, ← append_assoc] using ih h.prev theorem Slice.splits_prevn_endPos (s : Slice) (n : Nat) : - (s.endPos.prevn n).Splits (String.ofList (s.copy.toList.take (s.copy.length - n))) - (String.ofList (s.copy.toList.drop (s.copy.length - n))) := by + (s.endPos.prevn n).Splits (String.ofList (s.copy.toList.take (s.copy.toList.length - n))) + (String.ofList (s.copy.toList.drop (s.copy.toList.length - n))) := by simpa using s.splits_endPos.prevn n theorem Pos.Splits.prevn {s t₁ t₂ : String} {p : s.Pos} (h : p.Splits t₁ t₂) (n : Nat) : - (p.prevn n).Splits (String.ofList (t₁.toList.take (t₁.length - n))) (String.ofList (t₁.toList.drop (t₁.length - n)) ++ t₂) := by + (p.prevn n).Splits (String.ofList (t₁.toList.take (t₁.toList.length - n))) (String.ofList (t₁.toList.drop (t₁.toList.length - n)) ++ t₂) := by simpa [← splits_toSlice_iff, toSlice_prevn] using h.toSlice.prevn n theorem splits_prevn_endPos (s : String) (n : Nat) : - (s.endPos.prevn n).Splits (String.ofList (s.toList.take (s.length - n))) (String.ofList (s.toList.drop (s.length - n))) := by + (s.endPos.prevn n).Splits (String.ofList (s.toList.take (s.toList.length - n))) (String.ofList (s.toList.drop (s.toList.length - n))) := by simpa using s.splits_endPos.prevn n @[simp] diff --git a/src/Init/Data/String/Lemmas/TakeDrop.lean b/src/Init/Data/String/Lemmas/TakeDrop.lean index 83735c6d6c..3d6c988300 100644 --- a/src/Init/Data/String/Lemmas/TakeDrop.lean +++ b/src/Init/Data/String/Lemmas/TakeDrop.lean @@ -29,7 +29,7 @@ theorem dropEnd_eq_sliceTo {s : Slice} {n : Nat} : s.dropEnd n = s.sliceTo (s.en @[simp] theorem toList_copy_dropEnd {s : Slice} {n : Nat} : - (s.dropEnd n).copy.toList = s.copy.toList.take (s.copy.length - n) := by + (s.dropEnd n).copy.toList = s.copy.toList.take (s.copy.toList.length - n) := by simp [dropEnd_eq_sliceTo, (s.splits_prevn_endPos n).copy_sliceTo_eq] theorem take_eq_sliceTo {s : Slice} {n : Nat} : s.take n = s.sliceTo (s.startPos.nextn n) := @@ -44,7 +44,7 @@ theorem takeEnd_eq_sliceFrom {s : Slice} {n : Nat} : s.takeEnd n = s.sliceFrom ( @[simp] theorem toList_copy_takeEnd {s : Slice} {n : Nat} : - (s.takeEnd n).copy.toList = s.copy.toList.drop (s.copy.length - n) := by + (s.takeEnd n).copy.toList = s.copy.toList.drop (s.copy.toList.length - n) := by simp [takeEnd_eq_sliceFrom, (s.splits_prevn_endPos n).copy_sliceFrom_eq] end Slice @@ -63,7 +63,7 @@ theorem dropEnd_toSlice {s : String} {n : Nat} : s.toSlice.dropEnd n = s.dropEnd @[simp] theorem toList_copy_dropEnd {s : String} {n : Nat} : - (s.dropEnd n).copy.toList = s.toList.take (s.length - n) := by + (s.dropEnd n).copy.toList = s.toList.take (s.toList.length - n) := by simp [← dropEnd_toSlice] @[simp] @@ -80,7 +80,7 @@ theorem takeEnd_toSlice {s : String} {n : Nat} : s.toSlice.takeEnd n = s.takeEnd @[simp] theorem toList_copy_takeEnd {s : String} {n : Nat} : - (s.takeEnd n).copy.toList = s.toList.drop (s.length - n) := by + (s.takeEnd n).copy.toList = s.toList.drop (s.toList.length - n) := by simp [← takeEnd_toSlice] end String diff --git a/src/Init/Data/String/Length.lean b/src/Init/Data/String/Length.lean new file mode 100644 index 0000000000..d5f601f24c --- /dev/null +++ b/src/Init/Data/String/Length.lean @@ -0,0 +1,68 @@ +/- +Copyright (c) 2026 Lean FRO, LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Julia Markus Himmel +-/ +module + +prelude +public import Init.Data.String.Basic +import Init.Data.Char.Lemmas + +public section + +namespace String + +/-- +Returns the length of a string in Unicode code points. + +Examples: +* `"".length = 0` +* `"abc".length = 3` +* `"L∃∀N".length = 4` +-/ +@[extern "lean_string_length", expose, tagged_return] +def length (b : @& String) : Nat := + b.toList.length + +@[simp] +theorem Internal.size_toArray {b : String} : (String.Internal.toArray b).size = b.length := + (rfl) + +theorem length_toList {s : String} : s.toList.length = s.length := (rfl) + +@[deprecated String.length_toList (since := "2025-10-30")] +theorem length_data {b : String} : b.toList.length = b.length := (rfl) + +@[simp] +theorem length_ofList {l : List Char} : (String.ofList l).length = l.length := by + rw [← String.length_toList, String.toList_ofList] + +@[deprecated String.length_ofList (since := "2025-10-30")] +theorem _root_.List.length_asString {l : List Char} : (String.ofList l).length = l.length := + String.length_ofList + +@[simp] theorem length_empty : "".length = 0 := by simp [← length_toList, toList_empty] + +@[simp] +theorem length_singleton {c : Char} : (String.singleton c).length = 1 := by + simp [← length_toList] + +@[simp] theorem length_push (c : Char) : (String.push s c).length = s.length + 1 := by + simp [← length_toList] + +@[simp] theorem length_pushn (c : Char) (n : Nat) : (pushn s c n).length = s.length + n := by + rw [pushn_eq_repeat_push]; induction n <;> simp [Nat.repeat, Nat.add_assoc, *] + +@[simp] theorem length_append (s t : String) : (s ++ t).length = s.length + t.length := by + simp [← length_toList] + +end String + +namespace Char + +@[deprecated String.length_singleton (since := "2026-02-12")] +theorem length_toString (c : Char) : c.toString.length = 1 := by + simp + +end Char diff --git a/src/Init/Data/String/TakeDrop.lean b/src/Init/Data/String/TakeDrop.lean index f5f5ca62cc..b55ab71e52 100644 --- a/src/Init/Data/String/TakeDrop.lean +++ b/src/Init/Data/String/TakeDrop.lean @@ -28,7 +28,7 @@ open Slice.Pattern Returns a {name}`String.Slice` obtained by removing the specified number of characters (Unicode code points) from the start of the string. -If {name}`n` is greater than {lean}`s.length`, returns an empty slice. +If {name}`n` is greater than {lean}`s.toList.length`, returns an empty slice. This is a cheap operation because it does not allocate a new string to hold the result. To convert the result into a string, use {name}`String.Slice.copy`. @@ -50,7 +50,7 @@ def Internal.dropImpl (s : String) (n : Nat) : String := Returns a {name}`String.Slice` obtained by removing the specified number of characters (Unicode code points) from the end of the string. -If {name}`n` is greater than {lean}`s.length`, returns an empty slice. +If {name}`n` is greater than {lean}`s.toList.length`, returns an empty slice. This is a cheap operation because it does not allocate a new string to hold the result. To convert the result into a string, use {name}`String.Slice.copy`. @@ -80,7 +80,7 @@ def Internal.dropRightImpl (s : String) (n : Nat) : String := Returns a {name}`String.Slice` that contains the first {name}`n` characters (Unicode code points) of {name}`s`. -If {name}`n` is greater than {lean}`s.length`, returns {lean}`s.toSlice`. +If {name}`n` is greater than {lean}`s.toList.length`, returns {lean}`s.toSlice`. This is a cheap operation because it does not allocate a new string to hold the result. To convert the result into a string, use {name}`String.Slice.copy`. @@ -99,7 +99,7 @@ Examples: Returns a {name}`String.Slice` that contains the last {name}`n` characters (Unicode code points) of {name}`s`. -If {name}`n` is greater than {lean}`s.length`, returns {lean}`s.toSlice`. +If {name}`n` is greater than {lean}`s.toList.length`, returns {lean}`s.toSlice`. This is a cheap operation because it does not allocate a new string to hold the result. To convert the result into a string, use {name}`String.Slice.copy`. diff --git a/src/Init/System/FilePath.lean b/src/Init/System/FilePath.lean index b826958e25..bd42b49b7b 100644 --- a/src/Init/System/FilePath.lean +++ b/src/Init/System/FilePath.lean @@ -11,6 +11,7 @@ import Init.Data.String.Search public import Init.Data.ToString.Basic import Init.Data.Iterators.Consumers.Collect import Init.System.Platform +import Init.Data.String.Length public section diff --git a/src/Init/System/IO.lean b/src/Init/System/IO.lean index 4f529d57cd..64faef771c 100644 --- a/src/Init/System/IO.lean +++ b/src/Init/System/IO.lean @@ -1026,7 +1026,7 @@ and/or return data. partial def Handle.lines (h : Handle) : IO (Array String) := do let rec read (lines : Array String) := do let line ← h.getLine - if line.length == 0 then + if line.isEmpty then pure lines else if line.back == '\n' then let line := line.dropEnd 1 |>.copy @@ -1780,7 +1780,7 @@ and/or return data. partial def lines (s : Stream) : IO (Array String) := do let rec read (lines : Array String) := do let line ← s.getLine - if line.length == 0 then + if line.isEmpty then pure lines else if line.back == '\n' then let line := line.dropEnd 1 |>.copy diff --git a/src/Init/System/Uri.lean b/src/Init/System/Uri.lean index 95420a8c90..033252f293 100644 --- a/src/Init/System/Uri.lean +++ b/src/Init/System/Uri.lean @@ -13,6 +13,7 @@ import Init.Data.String.Search import Init.Omega import Init.System.Platform import Init.While +import Init.Data.String.Length public section diff --git a/src/Lean/Data/EditDistance.lean b/src/Lean/Data/EditDistance.lean index dff74584fd..6905dc5d3d 100644 --- a/src/Lean/Data/EditDistance.lean +++ b/src/Lean/Data/EditDistance.lean @@ -12,6 +12,7 @@ import Init.Data.Nat.Order import Init.Data.Order.Lemmas import Init.Data.Range import Init.While +import Init.Data.String.Length set_option linter.missingDocs true diff --git a/src/Lean/Data/Name.lean b/src/Lean/Data/Name.lean index 00b39c322f..1392ad175a 100644 --- a/src/Lean/Data/Name.lean +++ b/src/Lean/Data/Name.lean @@ -11,6 +11,7 @@ import Init.Data.String.TakeDrop import Init.Data.Ord.String import Init.Data.Ord.UInt import Init.Data.String.Search +import Init.Data.String.Length public section namespace Lean diff --git a/src/Lean/DocString/Links.lean b/src/Lean/DocString/Links.lean index d85813e5f9..0b57475bcb 100644 --- a/src/Lean/DocString/Links.lean +++ b/src/Lean/DocString/Links.lean @@ -12,6 +12,7 @@ import Init.Data.String.TakeDrop import Init.Data.String.Search import Init.Data.ToString.Macro import Init.While +import Init.Data.String.Length public section @@ -114,28 +115,28 @@ def rewriteManualLinksCore (s : String) : Id (Array (Lean.Syntax.Range × String let pre := iter iter := iter.next h - if !lookingAt scheme pre then + match pre.skip? scheme with + | none => out := out.push c continue - - let start := pre.nextn scheme.length - let mut iter' := start - while h' : ¬iter'.IsAtEnd do - let c' := iter'.get h' - let pre' := iter' - iter' := iter'.next h' - if urlChar c' && ¬iter'.IsAtEnd then - continue - match rw (s.extract start pre') with - | .error err => - errors := errors.push (⟨pre.offset, pre'.offset⟩, err) - out := out.push c - break - | .ok path => - out := out ++ manualRoot ++ path - out := out.push c' - iter := iter' - break + | some start => + let mut iter' := start + while h' : ¬iter'.IsAtEnd do + let c' := iter'.get h' + let pre' := iter' + iter' := iter'.next h' + if urlChar c' && ¬iter'.IsAtEnd then + continue + match rw (s.extract start pre') with + | .error err => + errors := errors.push (⟨pre.offset, pre'.offset⟩, err) + out := out.push c + break + | .ok path => + out := out ++ manualRoot ++ path + out := out.push c' + iter := iter' + break pure (errors, out) @@ -154,12 +155,6 @@ where c == '!' || c == '$' || c == '&' || c == '\'' || /- c == '(' || c == ')' || -/ c == '*' || c == '+' || c == ',' || c == ';' || c == '=' - /-- - Returns `true` if `goal` is a prefix of the string at the position pointed to by `iter`. - -/ - lookingAt (goal : String) {s : String} (iter : s.Pos) : Bool := - String.Pos.Raw.substrEq s iter.offset goal 0 goal.rawEndPos.byteIdx - /-- Rewrites Lean reference manual links in `docstring` to point at the reference manual. diff --git a/src/Lean/DocString/Markdown.lean b/src/Lean/DocString/Markdown.lean index 13c3aaa865..6abbe77d74 100644 --- a/src/Lean/DocString/Markdown.lean +++ b/src/Lean/DocString/Markdown.lean @@ -11,6 +11,7 @@ prelude public import Lean.DocString.Types public import Init.Data.String.TakeDrop public import Init.Data.String.Search +public import Init.Data.String.Length import Init.Data.ToString.Macro import Init.While diff --git a/src/Lean/Message.lean b/src/Lean/Message.lean index c82e6a18bb..d5552f455c 100644 --- a/src/Lean/Message.lean +++ b/src/Lean/Message.lean @@ -14,6 +14,7 @@ public import Lean.Util.Sorry import Init.Data.String.Search import Init.Data.Format.Macro import Init.Data.Iterators.Consumers.Collect +import Init.Data.String.Length public section @@ -688,7 +689,7 @@ def inlineExpr (e : Expr) (maxInlineLength := 30) : MessageData := (fun ctx => do let msg := MessageData.ofExpr e let render ← msg.formatExpensively ctx - if render.length > maxInlineLength || render.any (· == '\n') then + if render.positions.length > maxInlineLength || render.any (· == '\n') then return indentD msg ++ "\n" else return " `" ++ msg ++ "` ") @@ -704,7 +705,7 @@ def inlineExprTrailing (e : Expr) (maxInlineLength := 30) : MessageData := (fun ctx => do let msg := MessageData.ofExpr e let render ← msg.formatExpensively ctx - if render.length > maxInlineLength || render.any (· == '\n') then + if render.positions.length > maxInlineLength || render.any (· == '\n') then return indentD msg else return " `" ++ msg ++ "`") diff --git a/src/Lean/Meta/MethodSpecs.lean b/src/Lean/Meta/MethodSpecs.lean index 458332e874..fcbb9ef031 100644 --- a/src/Lean/Meta/MethodSpecs.lean +++ b/src/Lean/Meta/MethodSpecs.lean @@ -127,7 +127,7 @@ def mkSpecTheoremName (env : Environment) (instName : Name) (privateSpecs : Bool if privateSpecs then mkPrivateName env thmName else thmName def startsWithFollowedByNumber (s p : String) : Bool := - s.startsWith p && (s.drop p.length).isNat + (s.dropPrefix? p).any (·.isNat) def isSpecThmLikeSuffix (fieldName : Name) (s : String) : Bool := s == s!"{fieldName}_spec" || startsWithFollowedByNumber s s!"{fieldName}_spec_" diff --git a/src/Lean/Util/Path.lean b/src/Lean/Util/Path.lean index b2af1878ec..09c0253b70 100644 --- a/src/Lean/Util/Path.lean +++ b/src/Lean/Util/Path.lean @@ -18,6 +18,7 @@ import Init.Data.String.TakeDrop import Init.Data.List.Monadic import Init.Data.Option.BasicAux import Init.Data.ToString.Macro +import Init.Data.String.Length public section diff --git a/src/Lean/Widget/TaggedText.lean b/src/Lean/Widget/TaggedText.lean index 48895a5a4e..8b558bf91a 100644 --- a/src/Lean/Widget/TaggedText.lean +++ b/src/Lean/Widget/TaggedText.lean @@ -9,6 +9,7 @@ module prelude public import Lean.Server.Rpc.Basic import Init.Data.Array.GetLit +import Init.Data.String.Length public section diff --git a/src/Std/Http/Data/URI/Basic.lean b/src/Std/Http/Data/URI/Basic.lean index e4bb328344..4adddfc594 100644 --- a/src/Std/Http/Data/URI/Basic.lean +++ b/src/Std/Http/Data/URI/Basic.lean @@ -11,6 +11,7 @@ public import Std.Net public import Std.Http.Internal public import Std.Http.Data.URI.Encoding public import Init.Data.String.Search +public import Init.Data.String.Length public section diff --git a/src/Std/Internal/Parsec/String.lean b/src/Std/Internal/Parsec/String.lean index 851761c55b..746da76bc5 100644 --- a/src/Std/Internal/Parsec/String.lean +++ b/src/Std/Internal/Parsec/String.lean @@ -9,6 +9,7 @@ prelude public import Std.Internal.Parsec.Basic public import Init.Data.String.Slice public import Init.Data.String.Termination +import Init.Data.String.Length public section diff --git a/src/Std/Time/Duration.lean b/src/Std/Time/Duration.lean index 35ff4ca3d8..bfce6f135d 100644 --- a/src/Std/Time/Duration.lean +++ b/src/Std/Time/Duration.lean @@ -8,6 +8,7 @@ module prelude public import Std.Time.Date public import Init.Data.String.Basic +public import Init.Data.String.Length public section diff --git a/src/Std/Time/Format/Basic.lean b/src/Std/Time/Format/Basic.lean index 4f0f6a113d..a0da7d3293 100644 --- a/src/Std/Time/Format/Basic.lean +++ b/src/Std/Time/Format/Basic.lean @@ -442,7 +442,7 @@ inductive Modifier It takes a constructor function to build the `Modifier` and a classify function that maps the pattern length to a specific type. -/ private def parseMod (constructor : α → Modifier) (classify : Nat → Option α) (p : String) : Parser Modifier := - let len := p.length + let len := p.positions.length match classify len with | some res => pure (constructor res) | none => fail s!"invalid quantity of characters for '{p.front}'" @@ -617,16 +617,16 @@ private def specParse (s : String) : Except String FormatString := -- Pretty printer private def leftPad (n : Nat) (a : Char) (s : String) : String := - "".pushn a (n - s.length) ++ s + "".pushn a (n - s.positions.length) ++ s private def rightPad (n : Nat) (a : Char) (s : String) : String := - s ++ "".pushn a (n - s.length) + s ++ "".pushn a (n - s.positions.length) private def pad (size : Nat) (n : Int) (cut : Bool := false) : String := let (sign, n) := if n < 0 then ("-", -n) else ("", n) let numStr := toString n - if numStr.length > size then + if numStr.positions.length > size then sign ++ if cut then numStr.drop (numStr.length - size) |>.copy else numStr else sign ++ leftPad size '0' numStr diff --git a/src/lake/Lake/Util/Cli.lean b/src/lake/Lake/Util/Cli.lean index 22be41acb3..756ce27ce4 100644 --- a/src/lake/Lake/Util/Cli.lean +++ b/src/lake/Lake/Util/Cli.lean @@ -8,6 +8,7 @@ module prelude public import Init.Data.String.TakeDrop public import Init.Data.String.Search +public import Init.Data.String.Length namespace Lake diff --git a/src/lake/Lake/Util/String.lean b/src/lake/Lake/Util/String.lean index 5fc925216f..a2758022fd 100644 --- a/src/lake/Lake/Util/String.lean +++ b/src/lake/Lake/Util/String.lean @@ -10,6 +10,7 @@ public import Init.Data.ToString.Basic import Init.Data.UInt.Lemmas import Init.Data.String.Basic import Init.Data.Nat.Fold +import Init.Data.String.Length namespace Lake diff --git a/src/lake/Lake/Util/Version.lean b/src/lake/Lake/Util/Version.lean index bc897e7d13..fc4eb9fc09 100644 --- a/src/lake/Lake/Util/Version.lean +++ b/src/lake/Lake/Util/Version.lean @@ -13,6 +13,7 @@ import Init.Data.String.TakeDrop import Lean.Data.Trie import Init.Data.String.Search import Init.Omega +import Init.Data.String.Length /-! # Version