refactor: use String.Slice in String.take and variants (#11180)

This PR redefines `String.take` and variants to operate on
`String.Slice`. While previously functions returning a substring of the
input sometimes returned `String` and sometimes returned
`Substring.Raw`, they now uniformly return `String.Slice`.

This is a BREAKING change, because many functions now have a different
return type. So for example, if `s` is a string and `f` is a function
accepting a string, `f (s.drop 1)` will no longer compile because
`s.drop 1` is a `String.Slice`. To fix this, insert a call to `copy` to
restore the old behavior: `f (s.drop 1).copy`.

Of course, in many cases, there will be more efficient options. For
example, don't write `f <| s.drop 1 |>.copy |>.dropEnd 1 |>.copy`, write
`f <| s.drop 1 |>.dropEnd 1 |>.copy` instead. Also, instead of `(s.drop
1).copy = "Hello"`, write `s.drop 1 == "Hello".toSlice` instead.
This commit is contained in:
Markus Himmel 2025-11-18 17:13:48 +01:00 committed by GitHub
parent 03eb2f73ac
commit fa5d08b7de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 482 additions and 342 deletions

View file

@ -2785,23 +2785,6 @@ where
def substrEq (s1 : String) (pos1 : String.Pos.Raw) (s2 : String) (pos2 : String.Pos.Raw) (sz : Nat) : Bool :=
Pos.Raw.substrEq s1 pos1 s2 pos2 sz
/--
Checks whether the first string (`p`) is a prefix of the second (`s`).
`String.startsWith` is a version that takes the potential prefix after the string.
Examples:
* `"red".isPrefixOf "red green blue" = true`
* `"green".isPrefixOf "red green blue" = false`
* `"".isPrefixOf "red green blue" = true`
-/
def isPrefixOf (p : String) (s : String) : Bool :=
Pos.Raw.substrEq p 0 s 0 p.rawEndPos.byteIdx
@[export lean_string_isprefixof]
def Internal.isPrefixOfImpl (p : String) (s : String) : Bool :=
String.isPrefixOf p s
/--
Returns the position of the beginning of the line that contains the position `pos`.

View file

@ -10,6 +10,7 @@ public import Init.Data.String.Pattern
public import Init.Data.Ord.Basic
public import Init.Data.Iterators.Combinators.FilterMap
public import Init.Data.String.ToSlice
public import Init.Data.String.Termination
set_option doc.verso true
@ -403,19 +404,19 @@ Examples:
* {lean}`"red green blue".toSlice.dropWhile (fun (_ : Char) => true) == "".toSlice`
-/
@[inline]
partial def dropWhile [ForwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
go s
def dropWhile [ForwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
go s.startPos
where
@[specialize pat]
go (s : Slice) : Slice :=
if let some nextS := dropPrefix? s pat then
-- TODO: need lawful patterns to show this terminates
if s.startInclusive.offset < nextS.startInclusive.offset then
go nextS
go (curr : s.Pos) : Slice :=
if let some nextCurr := ForwardPattern.dropPrefix? (s.replaceStart curr) pat then
if curr < Pos.ofReplaceStart nextCurr then
go (Pos.ofReplaceStart nextCurr)
else
s
s.replaceStart curr
else
s
s.replaceStart curr
termination_by curr
/--
Removes leading whitespace from a slice by moving its start position to the first non-whitespace
@ -463,19 +464,19 @@ Examples:
* {lean}`"red green blue".toSlice.takeWhile (fun (_ : Char) => true) == "red green blue".toSlice`
-/
@[inline]
partial def takeWhile [ForwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
def takeWhile [ForwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
go s.startPos
where
@[specialize pat]
go (curr : s.Pos) : Slice :=
if let some nextCurr := ForwardPattern.dropPrefix? (s.replaceStart curr) pat then
if (s.replaceStart curr).startPos < nextCurr then
-- TODO: need lawful patterns to show this terminates
if curr < Pos.ofReplaceStart nextCurr then
go (Pos.ofReplaceStart nextCurr)
else
s.replaceEnd curr
else
s.replaceEnd curr
termination_by curr
/--
Finds the position of the first match of the pattern {name}`pat` in a slice {name}`s`. If there
@ -712,23 +713,23 @@ Examples:
* {lean}`"red green blue".toSlice.dropEndWhile (fun (_ : Char) => true) == "".toSlice`
-/
@[inline]
partial def dropEndWhile [BackwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
go s
def dropEndWhile [BackwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
go s.endPos
where
@[specialize pat]
go (s : Slice) : Slice :=
if let some nextS := dropSuffix? s pat then
-- TODO: need lawful patterns to show this terminates
if nextS.endExclusive.offset < s.endExclusive.offset then
go nextS
go (curr : s.Pos) : Slice :=
if let some nextCurr := BackwardPattern.dropSuffix? (s.replaceEnd curr) pat then
if Pos.ofReplaceEnd nextCurr < curr then
go (Pos.ofReplaceEnd nextCurr)
else
s
s.replaceEnd curr
else
s
s.replaceEnd curr
termination_by curr.down
/--
Removes trailing whitespace from a slice by moving its start position to the first non-whitespace
character, or to its end position if there is no non-whitespace character.
Removes trailing whitespace from a slice by moving its end position to the last non-whitespace
character, or to its start position if there is no non-whitespace character.
“Whitespace” is defined as characters for which {name}`Char.isWhitespace` returns {name}`true`.
@ -771,19 +772,19 @@ Examples:
* {lean}`"red green blue".toSlice.takeEndWhile (fun (_ : Char) => true) == "red green blue".toSlice`
-/
@[inline]
partial def takeEndWhile [BackwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
def takeEndWhile [BackwardPattern ρ] (s : Slice) (pat : ρ) : Slice :=
go s.endPos
where
@[specialize pat]
go (curr : s.Pos) : Slice :=
if let some nextCurr := BackwardPattern.dropSuffix? (s.replaceEnd curr) pat then
if nextCurr < (s.replaceEnd curr).endPos then
-- TODO: need lawful patterns to show this terminates
if Pos.ofReplaceEnd nextCurr < curr then
go (Pos.ofReplaceEnd nextCurr)
else
s.replaceStart curr
else
s.replaceStart curr
termination_by curr.down
/--
Finds the position of the first match of the pattern {name}`pat` in a slice {name}`true`, starting
@ -1338,4 +1339,36 @@ Examples:
def back (s : Slice) : Char :=
s.back?.getD default
/--
Appends the slices in a list of slices, placing the separator {name}`s` between each pair.
Examples:
* {lean}`", ".toSlice.intercalate ["red".toSlice, "green".toSlice, "blue".toSlice] = "red, green, blue"`
* {lean}`" and ".toSlice.intercalate ["tea".toSlice, "coffee".toSlice] = "tea and coffee"`
* {lean}`" | ".toSlice.intercalate ["M".toSlice, "".toSlice, "N".toSlice] = "M | | N"`
-/
def intercalate (s : Slice) : List Slice → String
| [] => ""
| a :: as => go a.copy s as
where go (acc : String) (s : Slice) : List Slice → String
| a :: as => go (acc ++ s ++ a) s as
| [] => acc
@[inherit_doc Slice.copy]
def toString (s : Slice) : String :=
s.copy
instance : ToString String.Slice where
toString := toString
/--
Converts a string to the Lean compiler's representation of names. The resulting name is
hierarchical, and the string is split at the dots ({lean}`'.'`).
{lean}`"a.b".toSlice.toName` is the name {lit}`a.b`, not {lit}`«a.b»`. For the latter, use
`Name.mkSimple`.
-/
def toName (s : Slice) : Lean.Name :=
s.toString.toName
end String.Slice

View file

@ -1,223 +1,320 @@
/-
Copyright (c) 2016 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Leonardo de Moura, Mario Carneiro
Author: Leonardo de Moura, Mario Carneiro, Markus Himmel
-/
module
prelude
public import Init.Data.String.Slice
public import Init.Data.String.Substring
set_option doc.verso true
/-!
# `String.take` and variants
This file contains the implementations of `String.take` and its variants. Currently, they are
implemented in terms of `Substring`; soon, they will be implemented in terms of `String.Slice`
instead.
This file contains the implementations of `String.take` and its variants.
-/
public section
namespace String
/--
Removes the specified number of characters (Unicode code points) from the start of the string.
variable {ρ : Type}
If `n` is greater than `s.length`, returns `""`.
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.
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`.
Examples:
* `"red green blue".drop 4 = "green blue"`
* `"red green blue".drop 10 = "blue"`
* `"red green blue".drop 50 = ""`
* {lean}`"red green blue".drop 4 == "green blue".toSlice`
* {lean}`"red green blue".drop 10 == "blue".toSlice`
* {lean}`"red green blue".drop 50 == "".toSlice`
* {lean}`"مرحبا بالعالم".drop 3 == "با بالعالم".toSlice`
-/
@[inline] def drop (s : String) (n : Nat) : String :=
(s.toRawSubstring.drop n).toString
@[inline] def drop (s : String) (n : Nat) : Slice :=
s.toSlice.drop n
@[export lean_string_drop]
def Internal.dropImpl (s : String) (n : Nat) : String :=
String.drop s n
(String.drop s n).copy
/--
Removes the specified number of characters (Unicode code points) from the end of the string.
Returns a {name}`String.Slice` obtained by removing the specified number of characters (Unicode code
points) from the end of the string.
If `n` is greater than `s.length`, returns `""`.
If {name}`n` is greater than {lean}`s.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`.
Examples:
* `"red green blue".dropRight 5 = "red green"`
* `"red green blue".dropRight 11 = "red"`
* `"red green blue".dropRight 50 = ""`
* {lean}`"red green blue".dropEnd 5 == "red green".toSlice`
* {lean}`"red green blue".dropEnd 11 == "red".toSlice`
* {lean}`"red green blue".dropEnd 50 == "".toSlice`
* {lean}`"مرحبا بالعالم".dropEnd 3 == "مرحبا بالع".toSlice`
-/
@[inline] def dropRight (s : String) (n : Nat) : String :=
(s.toRawSubstring.dropRight n).toString
@[inline] def dropEnd (s : String) (n : Nat) : Slice :=
s.toSlice.dropEnd n
@[deprecated String.dropEnd (since := "2025-11-14")]
def dropRight (s : String) (n : Nat) : String :=
(s.dropEnd n).copy
@[export lean_string_dropright]
def Internal.dropRightImpl (s : String) (n : Nat) : String :=
String.dropRight s n
(String.dropEnd s n).copy
/--
Creates a new string that contains the first `n` characters (Unicode code points) of `s`.
Returns a {name}`String.Slice` that contains the first {name}`n` characters (Unicode code points) of
{name}`s`.
If `n` is greater than `s.length`, returns `s`.
If {name}`n` is greater than {lean}`s.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`.
Examples:
* `"red green blue".take 3 = "red"`
* `"red green blue".take 1 = "r"`
* `"red green blue".take 0 = ""`
* `"red green blue".take 100 = "red green blue"`
* {lean}`"red green blue".take 3 == "red".toSlice`
* {lean}`"red green blue".take 1 == "r".toSlice`
* {lean}`"red green blue".take 0 == "".toSlice`
* {lean}`"red green blue".take 100 == "red green blue".toSlice`
* {lean}`"مرحبا بالعالم".take 5 == "مرحبا".toSlice`
-/
@[inline] def take (s : String) (n : Nat) : String :=
(s.toRawSubstring.take n).toString
@[inline] def take (s : String) (n : Nat) : String.Slice :=
s.toSlice.take n
/--
Creates a new string that contains the last `n` characters (Unicode code points) of `s`.
Returns a {name}`String.Slice` that contains the last {name}`n` characters (Unicode code points) of
{name}`s`.
If `n` is greater than `s.length`, returns `s`.
If {name}`n` is greater than {lean}`s.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`.
Examples:
* `"red green blue".takeRight 4 = "blue"`
* `"red green blue".takeRight 1 = "e"`
* `"red green blue".takeRight 0 = ""`
* `"red green blue".takeRight 100 = "red green blue"`
* {lean}`"red green blue".takeEnd 4 == "blue".toSlice`
* {lean}`"red green blue".takeEnd 1 == "e".toSlice`
* {lean}`"red green blue".takeEnd 0 == "".toSlice`
* {lean}`"red green blue".takeEnd 100 == "red green blue".toSlice`
* {lean}`"مرحبا بالعالم".takeEnd 5 == "لعالم".toSlice`
-/
@[inline] def takeRight (s : String) (n : Nat) : String :=
(s.toRawSubstring.takeRight n).toString
@[inline] def takeEnd (s : String) (n : Nat) : String.Slice :=
s.toSlice.takeEnd n
@[deprecated String.takeEnd (since := "2025-11-14")]
def takeRight (s : String) (n : Nat) : String :=
(s.takeEnd n).toString
/--
Creates a new string that contains the longest prefix of `s` in which `p` returns `true` for all
characters.
Creates a string slice that contains the longest prefix of {name}`s` in which {name}`pat` matched
(potentially repeatedly).
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".takeWhile (·.isLetter) = "red"`
* `"red green blue".takeWhile (· == 'r') = "r"`
* `"red green blue".takeWhile (· != 'n') = "red gree"`
* `"red green blue".takeWhile (fun _ => true) = "red green blue"`
* {lean}`"red green blue".takeWhile Char.isLower == "red".toSlice`
* {lean}`"red green blue".takeWhile 'r' == "r".toSlice`
* {lean}`"red red green blue".takeWhile "red " == "red red ".toSlice`
* {lean}`"red green blue".takeWhile (fun (_ : Char) => true) == "red green blue".toSlice`
-/
@[inline] def takeWhile (s : String) (p : Char → Bool) : String :=
(s.toRawSubstring.takeWhile p).toString
@[inline] def takeWhile [ForwardPattern ρ] (s : String) (pat : ρ) : String.Slice :=
s.toSlice.takeWhile pat
/--
Creates a new string by removing the longest prefix from `s` in which `p` returns `true` for all
characters.
Creates a string slice by removing the longest prefix from {name}`s` in which {name}`pat` matched
(potentially repeatedly).
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".dropWhile (·.isLetter) = " green blue"`
* `"red green blue".dropWhile (· == 'r') = "ed green blue"`
* `"red green blue".dropWhile (· != 'n') = "n blue"`
* `"red green blue".dropWhile (fun _ => true) = ""`
* {lean}`"red green blue".dropWhile Char.isLower == " green blue".toSlice`
* {lean}`"red green blue".dropWhile 'r' == "ed green blue".toSlice`
* {lean}`"red red green blue".dropWhile "red " == "green blue".toSlice`
* {lean}`"red green blue".dropWhile (fun (_ : Char) => true) == "".toSlice`
-/
@[inline] def dropWhile (s : String) (p : Char → Bool) : String :=
(s.toRawSubstring.dropWhile p).toString
@[inline] def dropWhile [ForwardPattern ρ] (s : String) (pat : ρ) : String.Slice :=
s.toSlice.dropWhile pat
/--
Creates a new string that contains the longest suffix of `s` in which `p` returns `true` for all
characters.
Creates a string slice that contains the longest suffix of {name}`s` in which {name}`pat` matched
(potentially repeatedly).
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".takeRightWhile (·.isLetter) = "blue"`
* `"red green blue".takeRightWhile (· == 'e') = "e"`
* `"red green blue".takeRightWhile (· != 'n') = " blue"`
* `"red green blue".takeRightWhile (fun _ => true) = "red green blue"`
* {lean}`"red green blue".takeEndWhile Char.isLower == "blue".toSlice`
* {lean}`"red green blue".takeEndWhile 'e' == "e".toSlice`
* {lean}`"red green blue".takeEndWhile (fun (_ : Char) => true) == "red green blue".toSlice`
-/
@[inline] def takeRightWhile (s : String) (p : Char → Bool) : String :=
(s.toRawSubstring.takeRightWhile p).toString
@[inline] def takeEndWhile [BackwardPattern ρ] (s : String) (pat : ρ) : String.Slice :=
s.toSlice.takeEndWhile pat
@[deprecated String.takeEndWhile (since := "2025-11-17")]
def takeRightWhile (s : String) (p : Char → Bool) : String :=
(s.takeEndWhile p).toString
/--
Creates a new string by removing the longest suffix from `s` in which `p` returns `true` for all
characters.
Creates a new string by removing the longest suffix from {name}`s` in which {name}`pat` matches
(potentially repeatedly).
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".dropRightWhile (·.isLetter) = "red green "`
* `"red green blue".dropRightWhile (· == 'e') = "red green blu"`
* `"red green blue".dropRightWhile (· != 'n') = "red green"`
* `"red green blue".dropRightWhile (fun _ => true) = ""`
* {lean}`"red green blue".dropEndWhile Char.isLower == "red green ".toSlice`
* {lean}`"red green blue".dropEndWhile 'e' == "red green blu".toSlice`
* {lean}`"red green blue".dropEndWhile (fun (_ : Char) => true) == "".toSlice`
-/
@[inline] def dropRightWhile (s : String) (p : Char → Bool) : String :=
(s.toRawSubstring.dropRightWhile p).toString
@[inline] def dropEndWhile [BackwardPattern ρ] (s : String) (pat : ρ) : String.Slice :=
s.toSlice.dropEndWhile pat
@[deprecated String.dropEndWhile (since := "2025-11-17")]
def dropRightWhile (s : String) (p : Char → Bool) : String :=
(s.dropEndWhile p).toString
/--
Checks whether the first string (`s`) begins with the second (`pre`).
Checks whether the first string ({name}`s`) begins with the pattern ({name}`pat`).
`String.isPrefix` is a version that takes the potential prefix before the string.
{name (scope := "Init.Data.String.TakeDrop")}`String.isPrefixOf` is a version that takes the
potential prefix before the string.
Examples:
* `"red green blue".startsWith "red" = true`
* `"red green blue".startsWith "green" = false`
* `"red green blue".startsWith "" = true`
* `"red".startsWith "red" = true`
* {lean}`"red green blue".startsWith "red" = true`
* {lean}`"red green blue".startsWith "green" = false`
* {lean}`"red green blue".startsWith "" = true`
* {lean}`"red green blue".startsWith 'r' = true`
* {lean}`"red green blue".startsWith Char.isLower = true`
-/
@[inline] def startsWith (s pre : String) : Bool :=
s.toRawSubstring.take pre.length == pre.toRawSubstring
@[inline] def startsWith [ForwardPattern ρ] (s : String) (pat : ρ) : Bool :=
s.toSlice.startsWith pat
/--
Checks whether the first string (`s`) ends with the second (`post`).
Checks whether the second string ({name}`s`) begins with a prefix ({name}`p`).
This function is generic over all currently supported patterns.
{name}`String.startsWith` is a version that takes the potential prefix after the string.
Examples:
* `"red green blue".endsWith "blue" = true`
* `"red green blue".endsWith "green" = false`
* `"red green blue".endsWith "" = true`
* `"red".endsWith "red" = true`
* {lean}`"red".isPrefixOf "red green blue" = true`
* {lean}`"green".isPrefixOf "red green blue" = false`
* {lean}`"".isPrefixOf "red green blue" = true`
-/
@[inline] def endsWith (s post : String) : Bool :=
s.toRawSubstring.takeRight post.length == post.toRawSubstring
@[inline] def isPrefixOf (p : String) (s : String) : Bool :=
s.startsWith p
@[export lean_string_isprefixof]
def Internal.isPrefixOfImpl (p : String) (s : String) : Bool :=
String.isPrefixOf p s
/--
Removes trailing whitespace from a string.
Checks whether the string ({name}`s`) ends with the pattern ({name}`pat`).
“Whitespace” is defined as characters for which `Char.isWhitespace` returns `true`.
This function is generic over all currently supported patterns.
Examples:
* `"abc".trimRight = "abc"`
* `" abc".trimRight = " abc"`
* `"abc \t ".trimRight = "abc"`
* `" abc ".trimRight = " abc"`
* `"abc\ndef\n".trimRight = "abc\ndef"`
* {lean}`"red green blue".endsWith "blue" = true`
* {lean}`"red green blue".endsWith "green" = false`
* {lean}`"red green blue".endsWith "" = true`
* {lean}`"red green blue".endsWith 'e' = true`
* {lean}`"red green blue".endsWith Char.isLower = true`
-/
@[inline] def trimRight (s : String) : String :=
s.toRawSubstring.trimRight.toString
@[inline] def endsWith [BackwardPattern ρ] (s : String) (pat : ρ) : Bool :=
s.toSlice.endsWith pat
/--
Removes leading whitespace from a string.
Removes trailing whitespace from a string by returning a slice whose end position is the last
non-whitespace character, or the start position if there is no non-whitespace character.
“Whitespace” is defined as characters for which `Char.isWhitespace` returns `true`.
“Whitespace” is defined as characters for which {name}`Char.isWhitespace` returns {name}`true`.
Examples:
* `"abc".trimLeft = "abc"`
* `" abc".trimLeft = " abc"`
* `"abc \t ".trimLeft = "abc \t "`
* `" abc ".trimLeft = "abc "`
* `"abc\ndef\n".trimLeft = "abc\ndef\n"`
* {lean}`"abc".trimAsciiEnd == "abc".toSlice`
* {lean}`" abc".trimAsciiEnd == " abc".toSlice`
* {lean}`"abc \t ".trimAsciiEnd == "abc".toSlice`
* {lean}`" abc ".trimAsciiEnd == " abc".toSlice`
* {lean}`"abc\ndef\n".trimAsciiEnd == "abc\ndef".toSlice`
-/
@[inline] def trimLeft (s : String) : String :=
s.toRawSubstring.trimLeft.toString
@[inline] def trimAsciiEnd (s : String) : String.Slice :=
s.toSlice.trimAsciiEnd
@[deprecated String.trimAsciiEnd (since := "2025-11-17")]
def trimRight (s : String) : String :=
s.trimAsciiEnd.copy
/--
Removes leading whitespace from a string by returning a slice whose start position is the first
non-whitespace character, or the end position if there is no non-whitespace character.
“Whitespace” is defined as characters for which {name}`Char.isWhitespace` returns {name}`true`.
Examples:
* {lean}`"abc".trimAsciiStart == "abc".toSlice`
* {lean}`" abc".trimAsciiStart == "abc".toSlice`
* {lean}`"abc \t ".trimAsciiStart == "abc \t ".toSlice`
* {lean}`" abc ".trimAsciiStart == "abc ".toSlice`
* {lean}`"abc\ndef\n".trimAsciiStart == "abc\ndef\n".toSlice`
-/
@[inline] def trimAsciiStart (s : String) : String.Slice :=
s.toSlice.trimAsciiStart
@[deprecated String.trimAsciiStart (since := "2025-11-17")]
def trimLeft (s : String) : String :=
s.trimAsciiStart.copy
/--
Removes leading and trailing whitespace from a string.
“Whitespace” is defined as characters for which `Char.isWhitespace` returns `true`.
“Whitespace” is defined as characters for which {name}`Char.isWhitespace` returns {name}`true`.
Examples:
* `"abc".trim = "abc"`
* `" abc".trim = "abc"`
* `"abc \t ".trim = "abc"`
* `" abc ".trim = "abc"`
* `"abc\ndef\n".trim = "abc\ndef"`
* {lean}`"abc".trimAscii == "abc".toSlice`
* {lean}`" abc".trimAscii == "abc".toSlice`
* {lean}`"abc \t ".trimAscii == "abc".toSlice`
* {lean}`" abc ".trimAscii == "abc".toSlice`
* {lean}`"abc\ndef\n".trimAscii == "abc\ndef".toSlice`
-/
@[inline] def trim (s : String) : String :=
s.toRawSubstring.trim.toString
@[inline] def trimAscii (s : String) : String.Slice :=
s.toSlice.trimAscii
@[deprecated String.trimAscii (since := "2025-11-17")]
def trim (s : String) : String :=
s.trimAscii.copy
@[export lean_string_trim]
def Internal.trimImpl (s : String) : String :=
String.trim s
(String.trimAscii s).copy
/--
Repeatedly increments a position in a string, as if by `String.next`, while the predicate `p`
returns `true` for the character at the position. Stops incrementing at the end of the string or
when `p` returns `false` for the current character.
Repeatedly increments a position in a string, as if by {name}`String.Pos.Raw.next`, while the
predicate {name}`p` returns {lean}`true` for the character at the position. Stops incrementing at
the end of the string or when {name}`p` returns {lean}`false` for the current character.
Examples:
* `let s := " a "; s.get (s.nextWhile Char.isWhitespace 0) = 'a'`
* `let s := "a "; s.get (s.nextWhile Char.isWhitespace 0) = 'a'`
* `let s := "ba "; s.get (s.nextWhile Char.isWhitespace 0) = 'b'`
* {lean}`let s := " a "; ((0 : Pos.Raw).nextWhile s Char.isWhitespace).get s = 'a'`
* {lean}`let s := "a "; ((0 : Pos.Raw).nextWhile s Char.isWhitespace).get s = 'a'`
* {lean}`let s := "ba "; (Pos.Raw.nextWhile s Char.isWhitespace 0).get s = 'b'`
-/
@[inline] def Pos.Raw.nextWhile (s : String) (p : Char → Bool) (i : String.Pos.Raw) : String.Pos.Raw :=
Substring.Raw.takeWhileAux s s.rawEndPos p i
@ -231,14 +328,14 @@ def Internal.nextWhileImpl (s : String) (p : Char → Bool) (i : String.Pos.Raw)
i.nextWhile s p
/--
Repeatedly increments a position in a string, as if by `String.next`, while the predicate `p`
returns `false` for the character at the position. Stops incrementing at the end of the string or
when `p` returns `true` for the current character.
Repeatedly increments a position in a string, as if by {name}`String.Pos.Raw.next`, while the predicate
{name}`p` returns {lean}`false` for the character at the position. Stops incrementing at the end of
the string or when {name}`p` returns {lean}`true` for the current character.
Examples:
* `let s := " a "; s.get (s.nextUntil Char.isWhitespace 0) = ' '`
* `let s := " a "; s.get (s.nextUntil Char.isLetter 0) = 'a'`
* `let s := "a "; s.get (s.nextUntil Char.isWhitespace 0) = ' '`
* {lean}`let s := " a "; (Pos.Raw.nextUntil s Char.isWhitespace 0).get s = ' '`
* {lean}`let s := " a "; (Pos.Raw.nextUntil s Char.isAlpha 0).get s = 'a'`
* {lean}`let s := "a "; (Pos.Raw.nextUntil s Char.isWhitespace 0).get s = ' '`
-/
@[inline] def Pos.Raw.nextUntil (s : String) (p : Char → Bool) (i : String.Pos.Raw) : String.Pos.Raw :=
nextWhile s (fun c => !p c) i
@ -248,67 +345,91 @@ def nextUntil (s : String) (p : Char → Bool) (i : String.Pos.Raw) : String.Pos
i.nextUntil s p
/--
If `pre` is a prefix of `s`, returns the remainder. Returns `none` otherwise.
If {name}`pat` matches a prefix of {name}`s`, returns the remainder. Returns {name}`none` otherwise.
The string `pre` is a prefix of `s` if there exists a `t : String` such that `s = pre ++ t`. If so,
the result is `some t`.
Use {name (scope := "Init.Data.String.Slice")}`String.dropPrefix` to return the slice
unchanged when {name}`pat` does not match a prefix.
Use `String.stripPrefix` to return the string unchanged when `pre` is not a prefix.
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".dropPrefix? "red " = some "green blue"`
* `"red green blue".dropPrefix? "reed " = none`
* `"red green blue".dropPrefix? "" = some "red green blue"`
* {lean}`"red green blue".dropPrefix? "red " == some "green blue".toSlice`
* {lean}`"red green blue".dropPrefix? "reed " == none`
* {lean}`"red green blue".dropPrefix? 'r' == some "ed green blue".toSlice`
* {lean}`"red green blue".dropPrefix? Char.isLower == some "ed green blue".toSlice`
-/
def dropPrefix? (s : String) (pre : String) : Option Substring.Raw :=
s.toRawSubstring.dropPrefix? pre.toRawSubstring
def dropPrefix? [ForwardPattern ρ] (s : String) (pat : ρ) : Option String.Slice :=
s.toSlice.dropPrefix? pat
/--
If `suff` is a suffix of `s`, returns the remainder. Returns `none` otherwise.
If {name}`pat` matches a suffix of {name}`s`, returns the remainder. Returns {name}`none` otherwise.
The string `suff` is a suffix of `s` if there exists a `t : String` such that `s = t ++ suff`. If so,
the result is `some t`.
Use {name (scope := "Init.Data.String.TakeDrop")}`String.dropSuffix` to return the slice
unchanged when {name}`pat` does not match a prefix.
Use `String.stripSuffix` to return the string unchanged when `suff` is not a suffix.
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".dropSuffix? " blue" = some "red green"`
* `"red green blue".dropSuffix? " blu " = none`
* `"red green blue".dropSuffix? "" = some "red green blue"`
* {lean}`"red green blue".dropSuffix? " blue" == some "red green".toSlice`
* {lean}`"red green blue".dropSuffix? "bluu " == none`
* {lean}`"red green blue".dropSuffix? 'e' == some "red green blu".toSlice`
* {lean}`"red green blue".dropSuffix? Char.isLower == some "red green blu".toSlice`
-/
def dropSuffix? (s : String) (suff : String) : Option Substring.Raw :=
s.toRawSubstring.dropSuffix? suff.toRawSubstring
def dropSuffix? [BackwardPattern ρ] (s : String) (pat : ρ) : Option String.Slice :=
s.toSlice.dropSuffix? pat
/--
If `pre` is a prefix of `s`, returns the remainder. Returns `s` unmodified otherwise.
If {name}`pat` matches a prefix of {name}`s`, returns the remainder. Returns {name}`s` unmodified
otherwise.
The string `pre` is a prefix of `s` if there exists a `t : String` such that `s = pre ++ t`. If so,
the result is `t`. Otherwise, it is `s`.
Use {name}`String.dropPrefix?` to return {name}`none` when {name}`pat` does not match a prefix.
Use `String.dropPrefix?` to return `none` when `pre` is not a prefix.
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".stripPrefix "red " = "green blue"`
* `"red green blue".stripPrefix "reed " = "red green blue"`
* `"red green blue".stripPrefix "" = "red green blue"`
* {lean}`"red green blue".dropPrefix "red " == "green blue".toSlice`
* {lean}`"red green blue".dropPrefix "reed " == "red green blue".toSlice`
* {lean}`"red green blue".dropPrefix 'r' == "ed green blue".toSlice`
* {lean}`"red green blue".dropPrefix Char.isLower == "ed green blue".toSlice`
-/
def stripPrefix (s : String) (pre : String) : String :=
s.dropPrefix? pre |>.map Substring.Raw.toString |>.getD s
def dropPrefix [ForwardPattern ρ] (s : String) (pat : ρ) : String.Slice :=
s.toSlice.dropPrefix pat
@[deprecated String.dropPrefix (since := "2025-11-17")]
def stripPrefix (s pre : String) : String :=
(s.dropPrefix pre).toString
/--
If `suff` is a suffix of `s`, returns the remainder. Returns `s` unmodified otherwise.
If {name}`pat` matches a suffix of {name}`s`, returns the remainder. Returns {name}`s` unmodified
otherwise.
The string `suff` is a suffix of `s` if there exists a `t : String` such that `s = t ++ suff`. If so,
the result is `t`. Otherwise, it is `s`.
Use {name}`String.dropSuffix?` to return {name}`none` when {name}`pat` does not match a prefix.
Use `String.dropSuffix?` to return `none` when `suff` is not a suffix.
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`.
This function is generic over all currently supported patterns.
Examples:
* `"red green blue".stripSuffix " blue" = "red green"`
* `"red green blue".stripSuffix " blu " = "red green blue"`
* `"red green blue".stripSuffix "" = "red green blue"`
* {lean}`"red green blue".dropSuffix " blue" == "red green".toSlice`
* {lean}`"red green blue".dropSuffix "bluu " == "red green blue".toSlice`
* {lean}`"red green blue".dropSuffix 'e' == "red green blu".toSlice`
* {lean}`"red green blue".dropSuffix Char.isLower == "red green blu".toSlice`
-/
def dropSuffix [BackwardPattern ρ] (s : String) (pat : ρ) : String.Slice :=
s.toSlice.dropSuffix pat
@[deprecated String.dropSuffix (since := "2025-11-17")]
def stripSuffix (s : String) (suff : String) : String :=
s.dropSuffix? suff |>.map Substring.Raw.toString |>.getD s
(s.dropSuffix suff).toString
end String

View file

@ -7,6 +7,7 @@ module
prelude
public import Init.Data.String.Substring
import Init.Data.String.TakeDrop
/-!
Here we give the. implementation of `Name.toString`. There is also a private implementation in

View file

@ -1008,8 +1008,8 @@ partial def Handle.lines (h : Handle) : IO (Array String) := do
if line.length == 0 then
pure lines
else if line.back == '\n' then
let line := line.dropRight 1
let line := if line.back == '\r' then line.dropRight 1 else line
let line := line.dropEnd 1 |>.copy
let line := if line.back == '\r' then line.dropEnd 1 |>.copy else line
read <| lines.push line
else
pure <| lines.push line
@ -1791,8 +1791,8 @@ partial def lines (s : Stream) : IO (Array String) := do
if line.length == 0 then
pure lines
else if line.back == '\n' then
let line := line.dropRight 1
let line := if line.back == '\r' then line.dropRight 1 else line
let line := line.dropEnd 1 |>.copy
let line := if line.back == '\r' then line.dropEnd 1 |>.copy else line
read <| lines.push line
else
pure <| lines.push line

View file

@ -107,13 +107,13 @@ def fileUriToPath? (uri : String) : Option System.FilePath := Id.run do
if !uri.startsWith "file://" then
none
else
let mut p := (unescapeUri uri).drop "file://".length
p := p.dropWhile (λ c => c != '/') -- drop the hostname.
let mut p := (unescapeUri uri).drop "file://".length |>.copy
p := p.dropWhile (λ c => c != '/') |>.copy -- drop the hostname.
-- On Windows, the path "/c:/temp" needs to become "C:/temp"
if System.Platform.isWindows && p.length >= 2 &&
p.front == '/' && (String.Pos.Raw.get p ⟨1⟩).isAlpha && String.Pos.Raw.get p ⟨2⟩ == ':' then
-- see also `pathToUri`
p := String.Pos.Raw.modify (p.drop 1) 0 .toUpper
p := String.Pos.Raw.modify (p.drop 1).copy 0 .toUpper
some p
end Uri

View file

@ -7,6 +7,7 @@ module
prelude
public import Lean.Environment
import Init.Data.String.TakeDrop
public section

View file

@ -21,14 +21,14 @@ open Lean.JsonRpc
section
private def parseHeaderField (s : String) : Option (String × String) := do
guard $ s ≠ "" ∧ s.takeRight 2 = "\r\n"
let xs := (s.dropRight 2).splitOn ": "
guard $ s ≠ "" ∧ s.takeEnd 2 == "\r\n".toSlice
let xs := (s.dropEnd 2).split ": " |>.toList
match xs with
| [] => none
| [_] => none
| name :: value :: rest =>
let value := ": ".intercalate (value :: rest)
some ⟨name, value⟩
let value := ": ".toSlice.intercalate (value :: rest)
some ⟨name.copy, value⟩
/-- Returns true when the string is a Lean 3 request.
This means that the user is running a Lean 3 language client that

View file

@ -187,8 +187,8 @@ where
let (pre, post) := go more
(s ++ pre, post)
else
let s1 := s.takeWhile (·.isWhitespace)
let s2 := s.drop s1.length
let s1 := s.takeWhile Char.isWhitespace |>.copy
let s2 := s.drop s1.length |>.copy
(s1, .text s2 ++ .concat more.toArray)
| .concat xs :: more => go (xs.toList ++ more)
| here :: more => ("", here ++ .concat more.toArray)
@ -202,8 +202,8 @@ where
let (pre, post) := go more
(pre, post ++ s)
else
let s1 := s.takeRightWhile (·.isWhitespace)
let s2 := s.dropRight s1.length
let s1 := s.takeEndWhile Char.isWhitespace |>.copy
let s2 := s.dropEnd s1.length |>.copy
(.concat more.toArray.reverse ++ .text s2, s1)
| .concat xs :: more => go (xs.reverse.toList ++ more)
| here :: more => (.concat more.toArray.reverse ++ here, "")

View file

@ -683,7 +683,7 @@ mutual
let str := s.stxStack.back
if let .atom info str := str then
if str.startsWith "\" " && str.endsWith " \"" then
let core := str.drop 2 |>.dropRight 2
let core := str.drop 2 |>.dropEnd 2 |>.copy
if core.any (· != ' ') then
let str := "\"" ++ core ++ "\""
let info : SourceInfo :=
@ -1047,7 +1047,7 @@ mutual
| other => s.mkError s!"Internal error - not a column node {other}"
deIndent (n : Nat) (str : String) : String := Id.run do
let str := if str != "" && str.back == '\n' then str.dropRight 1 else str
let str := if str != "" && str.back == '\n' then str.dropEnd 1 |>.copy else str
let mut out := ""
for line in str.splitOn "\n" do
out := out ++ line.drop n ++ "\n"

View file

@ -597,7 +597,7 @@ def elabCommandTopLevel (stx : Syntax) : CommandElabM Unit := withRef stx do pro
stx.hasMissing && !showPartialSyntaxErrors.get (← getOptions) }) do
-- initialize quotation context using hash of input string
let ss? := stx.getSubstring? (withLeading := false) (withTrailing := false)
withInitQuotContext (ss?.map (hash ·.toString.trim)) do
withInitQuotContext (ss?.map (hash ·.toString.trimAscii.copy)) do
-- Reset messages and info state, which are both per-command
modify fun st => { st with messages := {}, infoState := { enabled := st.infoState.enabled } }
try

View file

@ -29,7 +29,7 @@ def mkFromJsonHeader (indVal : InductiveVal) : TermElabM Header := do
def mkJsonField (n : Name) : CoreM (Bool × Term) := do
let .str .anonymous s := n | throwError "invalid json field name {n}"
let s₁ := s.dropRightWhile (· == '?')
let s₁ := s.dropEndWhile (· == '?') |>.copy
return (s != s₁, Syntax.mkStrLit s₁)
def mkToJsonBodyForStruct (header : Header) (indName : Name) : TermElabM Term := do

View file

@ -156,7 +156,7 @@ def name (full : Option Ident := none) (scope : DocScope := .local)
let x := s.getString.toName
if x.isAnonymous then
let h ←
if s.getString != s.getString.trim && !s.getString.trim.isEmpty then
if s.getString.toSlice != s.getString.trimAscii && !s.getString.trimAscii.isEmpty then
-- Like Markdown, Verso code elements that start and end with a space will strip the space,
-- to allow code with leading or trailing backticks. But our suggestions shouldn't prefer
-- that form here. Thus, the suggestion uses the delimiter positions instead of the string
@ -170,7 +170,7 @@ def name (full : Option Ident := none) (scope : DocScope := .local)
let ⟨tailPos, _⟩ ← tk2.getRange?
pure <| Syntax.mkStrLit (String.Pos.Raw.extract text.source pos tailPos) (info := .synthetic pos tailPos)
if let some ref := ref? then
m!"Remove surrounding whitespace:".hint #[s.getString.trim] (ref? := some ref)
m!"Remove surrounding whitespace:".hint #[s.getString.trimAscii.copy] (ref? := some ref)
else pure m!""
else pure m!""
throwErrorAt s "Not a valid name.{h}"
@ -870,7 +870,7 @@ where
| (output, .error e) => Lean.logError e.toMessageData; pure (output, cmdState)
| (output, .ok ((), cmdState)) => pure (output, cmdState)
if output.trim.isEmpty then return cmdState
if output.trimAscii.isEmpty then return cmdState
let log : MessageData → Command.CommandElabM Unit :=
if let some tok := firstToken? stx then logInfoAt tok
@ -910,7 +910,7 @@ def output (name : Ident) (severity : Option (WithSyntax MessageSeverity) := non
return .code code.getString
let codeStr := code.getString
for (sev, out) in outs do
if out.trim == codeStr.trim then
if out.trimAscii == codeStr.trimAscii then
if let some s := severity then
if s.val != sev then
let sevName :=

View file

@ -54,14 +54,14 @@ private partial def containsAtom (e : Expr) (atom : String) : MetaM Bool := do
| (``ParserDescr.trailingNode, #[_, _, _, p]) => containsAtom p atom
| (``ParserDescr.unary, #[.app _ (.lit (.strVal _)), p]) => containsAtom p atom
| (``ParserDescr.binary, #[.app _ (.lit (.strVal "andthen")), p, _]) => containsAtom p atom
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => pure (tk.trim == atom)
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => pure (tk.trim == atom)
| (``ParserDescr.nonReservedSymbol, #[.lit (.strVal tk), _]) => pure (tk.trimAscii == atom.toSlice)
| (``ParserDescr.symbol, #[.lit (.strVal tk)]) => pure (tk.trimAscii == atom.toSlice)
| (``Parser.withAntiquot, #[_, p]) => containsAtom p atom
| (``Parser.leadingNode, #[_, _, p]) => containsAtom p atom
| (``HAndThen.hAndThen, #[_, _, _, _, p1, p2]) =>
containsAtom p1 atom <||> containsAtom p2 atom
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => pure (tk.trim == atom)
| (``Parser.symbol, #[.lit (.strVal tk)]) => pure (tk.trim == atom)
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) => pure (tk.trimAscii == atom.toSlice)
| (``Parser.symbol, #[.lit (.strVal tk)]) => pure (tk.trimAscii == atom.toSlice)
| (``Parser.symbol, #[_nonlit]) => pure false
| (``Parser.withCache, #[_, p]) => containsAtom p atom
| _ => if tryWhnf then attempt (← Meta.whnf p) false else pure false
@ -81,7 +81,7 @@ private partial def containsAtom' (e : Expr) (atom : String) : MetaM (Option Exp
| (``ParserDescr.symbol, #[.lit (.strVal tk)])
| (``Parser.symbol, #[.lit (.strVal tk)])
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) =>
if tk.trim == atom then
if tk.trimAscii == atom.toSlice then
pure (Expr.app (.const ``ParserDescr.const []) (toExpr ``Parser.skip))
else pure none
| (``Parser.withAntiquot, #[_, p]) => containsAtom' p atom
@ -132,7 +132,7 @@ private partial def startsWithAtom? (e : Expr) (atom : String) : MetaM (Option E
| (``ParserDescr.symbol, #[.lit (.strVal tk)])
| (``Parser.symbol, #[.lit (.strVal tk)])
| (``Parser.nonReservedSymbol, #[.lit (.strVal tk), _]) =>
if tk.trim == atom then
if tk.trimAscii == atom.toSlice then
pure (Expr.app (.const ``ParserDescr.const []) (toExpr ``Parser.skip))
else pure none
| (``Parser.withAntiquot, #[_, p]) => startsWithAtom? p atom
@ -192,7 +192,7 @@ where
| [] => return false
| .ident .. :: _ => return false
| .atom _ s :: ss =>
if a.trim == s.trim then
if a.trimAscii == s.trimAscii then
set as
go ss
else return false
@ -236,7 +236,7 @@ private def parserDescrHasAtom (atom : String) (p : ParserDescr) : TermElabM (Op
| .node _ _ p | .trailingNode _ _ _ p | .unary _ p =>
parserDescrHasAtom atom p
| .nonReservedSymbol tk _ | .symbol tk =>
if tk.trim == atom then
if tk.trimAscii == atom.toSlice then
pure (some (.const ``Parser.skip))
else pure none
| .binary ``Parser.andthen p1 p2 =>
@ -258,7 +258,7 @@ private def parserDescrStartsWithAtom (atom : String) (p : ParserDescr) : TermEl
| .node _ _ p | .trailingNode _ _ _ p | .unary _ p =>
parserDescrStartsWithAtom atom p
| .nonReservedSymbol tk _ | .symbol tk =>
if tk.trim == atom then
if tk.trimAscii == atom.toSlice then
pure (some (.const ``Parser.skip))
else pure none
| .binary ``Parser.andthen p1 p2 =>

View file

@ -179,7 +179,7 @@ def MessageOrdering.apply (mode : MessageOrdering) (msgs : List String) : List S
@[builtin_command_elab Lean.guardMsgsCmd] def elabGuardMsgs : CommandElab
| `(command| $[$dc?:docComment]? #guard_msgs%$tk $(spec?)? in $cmd) => do
let expected : String := (← dc?.mapM (getDocStringText ·)).getD ""
|>.trim |> removeTrailingWhitespaceMarker
|>.trimAscii |>.copy |> removeTrailingWhitespaceMarker
let { whitespace, ordering, filterFn, reportPositions } ← parseGuardMsgsSpec spec?
-- do not forward snapshot as we don't want messages assigned to it to leak outside
withReader ({ · with snap? := none }) do
@ -206,7 +206,7 @@ def MessageOrdering.apply (mode : MessageOrdering) (msgs : List String) : List S
else none
let strings ← toCheck.toList.mapM (messageToString · reportPos?)
let strings := ordering.apply strings
let res := "---\n".intercalate strings |>.trim
let res := "---\n".intercalate strings |>.trimAscii |>.copy
if whitespace.apply expected == whitespace.apply res then
-- Passed. Only put toPassthrough messages back on the message log
modify fun st => { st with messages := toPassthrough }

View file

@ -45,7 +45,7 @@ private partial def floatOutAntiquotTerms (stx : Syntax) : StateT (Syntax → Te
private def getSepFromSplice (splice : Syntax) : String :=
if let Syntax.atom _ sep := getAntiquotSpliceSuffix splice then
sep.dropRight 1 -- drop trailing *
sep.dropEnd 1 |>.copy -- drop trailing *
else
unreachable!

View file

@ -244,7 +244,7 @@ where
isValidAtom (s : String) : Bool :=
-- Pretty-printing instructions shouldn't affect validity
let s := s.trim
let s := s.trimAscii.copy
!s.isEmpty &&
(s.front != '\'' || "''".isPrefixOf s) &&
s.front != '\"' &&
@ -331,7 +331,7 @@ private partial def mkNameFromParserSyntax (catName : Name) (stx : Syntax) : Mac
where
visit (stx : Syntax) (acc : String) : String :=
match stx.isStrLit? with
| some val => acc ++ (val.trim.map fun c => if c.isWhitespace then '_' else c).capitalize
| some val => acc ++ (val.trimAscii.copy.map fun c => if c.isWhitespace then '_' else c).capitalize
| none =>
match stx with
| Syntax.node _ k args =>

View file

@ -375,7 +375,7 @@ def elabInvariants (stx : Syntax) (invariants : Array MVarId) (suggestInvariant
let n? : Option Nat := do
let `(binderIdent| $tag:ident) := tag | some n -- fall back to ordinal
let .str .anonymous s := tag.getId | none
s.dropPrefix? "inv" >>= Substring.Raw.toNat?
s.dropPrefix? "inv" >>= String.Slice.toNat?
let some mv := do invariants[(← n?) - 1]? | do
logErrorAt alt m!"No invariant with label {tag} {repr tag}."
continue

View file

@ -78,7 +78,7 @@ private def showParserName (n : Name) : MetaM MessageData := do
let tok ←
if let some descr := env.find? n |>.bind (·.value?) then
if let some tk ← getFirstTk descr then
pure <| Std.Format.text tk.trim
pure <| Std.Format.text tk.trimAscii.copy
else pure <| format n
else pure <| format n
pure <| .ofFormatWithInfos {
@ -166,7 +166,7 @@ def allTacticDocs : MetaM (Array TacticDoc) := do
let userName : String ←
if let some descr := env.find? tac |>.bind (·.value?) then
if let some tk ← getFirstTk descr then
pure tk.trim
pure tk.trimAscii.copy
else pure tac.toString
else pure tac.toString

View file

@ -342,7 +342,7 @@ private def expandUserTactic (tac : TSyntax `tactic) (goal : MVarId) : MetaM (Ar
if line.startsWith " [apply] " then
let tacticText := line.drop " [apply] ".length
let env ← getEnv
if let .ok stx := Parser.runParserCategory env `tactic tacticText then
if let .ok stx := Parser.runParserCategory env `tactic tacticText.copy then
suggestions := suggestions.push ⟨stx⟩
pure (some suggestions))

View file

@ -154,7 +154,7 @@ deriving Repr, Inhabited
private def ValidationState.ofSource (input : String) : ValidationState where
lines := input.splitOn "\n"
|>.zipIdx
|>.filter (!·.1.trim.isEmpty)
|>.filter (!·.1.trimAscii.isEmpty)
|>.toArray
-- Workaround to account for the fact that `Input` expects "EOF" to be a valid position
@ -258,9 +258,9 @@ where
let (_, closing) ← fence numTicks
<|> fail s!"Missing closing code fence for block with header '{infoString}'"
-- Validate code block:
unless closing.trim.isEmpty do
unless closing.trimAscii.isEmpty do
fail s!"Expected a closing code fence, but found the nonempty info string `{closing}`"
let info ← match ErrorExplanation.CodeInfo.parse infoString with
let info ← match ErrorExplanation.CodeInfo.parse infoString.copy with
| .ok i => pure i
| .error s =>
fail s
@ -277,7 +277,7 @@ where
fence (ticksToClose : Option Nat := none) := attempt do
let line ← any
if line.startsWith "```" then
let numTicks := line.takeWhile (· == '`') |>.length
let numTicks := line.takeWhile (· == '`') |>.copy |>.length
match ticksToClose with
| none => return (numTicks, line.drop numTicks)
| some n =>

View file

@ -157,7 +157,7 @@ def bitVecWidths (t : InfoTree) : List (Syntax × Name) :=
/-- Strip optional suffixes from a binder name. -/
def stripBinderName (s : String) : String :=
s.stripSuffix "'" |>.stripSuffix "₁" |>.stripSuffix "₂" |>.stripSuffix "₃" |>.stripSuffix "₄"
s.dropSuffix "'" |>.dropSuffix "₁" |>.dropSuffix "₂" |>.dropSuffix "₃" |>.dropSuffix "₄" |>.copy
/-- Allowed names for index variables. -/
def allowedIndices : List String := ["i", "j", "k", "start", "stop", "step"]

View file

@ -97,7 +97,7 @@ def loadPlugin (path : System.FilePath) : IO Unit := do
let dynlib ← Dynlib.load path
-- Lean libraries can be prefixed with `lib` or suffixed with `_shared`
-- under some configurations. We strip these from the initializer symbol.
let name := name.stripPrefix "lib" |>.stripSuffix "_shared"
let name := name.dropPrefix "lib" |>.dropSuffix "_shared"
let name := s!"initialize_{name}"
let some sym := dynlib.get? name
| throw <| IO.userError s!"error loading plugin, initializer not found '{name}'"

View file

@ -1111,7 +1111,7 @@ def symbolFn (sym : String) : ParserFn :=
symbolFnAux sym ("'" ++ sym ++ "'")
def symbolNoAntiquot (sym : String) : Parser :=
let sym := sym.trim
let sym := sym.trimAscii.copy
{ info := symbolInfo sym
fn := symbolFn sym }
@ -1154,7 +1154,7 @@ def nonReservedSymbolInfo (sym : String) (includeIdent : Bool) : ParserInfo := {
}
def nonReservedSymbolNoAntiquot (sym : String) (includeIdent := false) : Parser :=
let sym := sym.trim
let sym := sym.trimAscii.copy
{ info := nonReservedSymbolInfo sym includeIdent,
fn := nonReservedSymbolFn sym }
@ -1236,8 +1236,8 @@ def unicodeSymbolFn (sym asciiSym : String) : ParserFn :=
set_option linter.unusedVariables false in
def unicodeSymbolNoAntiquot (sym asciiSym : String) (preserveForPP : Bool) : Parser :=
let sym := sym.trim
let asciiSym := asciiSym.trim
let sym := sym.trimAscii.copy
let asciiSym := asciiSym.trimAscii.copy
{ info := unicodeSymbolInfo sym asciiSym
fn := unicodeSymbolFn sym asciiSym }
@ -1893,7 +1893,7 @@ def nodeWithAntiquot (name : String) (kind : SyntaxNodeKind) (p : Parser) (anony
-- =========================
def sepByElemParser (p : Parser) (sep : String) : Parser :=
withAntiquotSpliceAndSuffix `sepBy p (symbol (sep.trim ++ "*"))
withAntiquotSpliceAndSuffix `sepBy p (symbol (sep.trimAscii.copy ++ "*"))
def sepBy (p : Parser) (sep : String) (psep : Parser := symbol sep) (allowTrailingSep : Bool := false) : Parser :=
sepByNoAntiquot (sepByElemParser p sep) psep allowTrailingSep

View file

@ -260,7 +260,7 @@ def getTacticExtensions (env : Environment) (tactic : Name) : Array String := Id
def getTacticExtensionString (env : Environment) (tactic : Name) : String := Id.run do
let exts := getTacticExtensions env tactic
if exts.size == 0 then ""
else "\n\nExtensions:\n\n" ++ String.join (exts.toList.map bullet) |>.trimRight
else "\n\nExtensions:\n\n" ++ String.join (exts.toList.map bullet) |>.trimAsciiEnd |>.copy
where
indentLine (str: String) : String :=
(if str.all (·.isWhitespace) then str else " " ++ str) ++ "\n"

View file

@ -72,7 +72,7 @@ the docstring. -/
def getRecommendedSpellingString (env : Environment) (declName : Name) : String := Id.run do
let spellings := getRecommendedSpellingsForName env declName
if spellings.size == 0 then ""
else "\n\nConventions for notations in identifiers:\n\n" ++ String.join (spellings.toList.map bullet) |>.trimRight
else "\n\nConventions for notations in identifiers:\n\n" ++ String.join (spellings.toList.map bullet) |>.trimAsciiEnd |>.copy
where
indentLine (str : String) : String :=
(if str.all (·.isWhitespace) then str else " " ++ str) ++ "\n"
@ -81,7 +81,7 @@ where
let additionalInfoLines := spelling.additionalInformation?.map (·.splitOn "\n")
match additionalInfoLines with
| none | some [] => firstLine ++ ".\n\n"
| some [l] => firstLine ++ s!" ({l.trimRight}).\n\n"
| some [l] => firstLine ++ s!" ({l.trimAsciiEnd}).\n\n"
| some ls => firstLine ++ ".\n\n" ++ String.join (ls.map indentLine) ++ "\n\n"
end Lean.Parser.Term.Doc

View file

@ -380,14 +380,14 @@ def pushToken (info : SourceInfo) (tk : String) (ident : Bool) : FormatterM Unit
let st ← get
-- If there is no space between `tk` and the next word, see if we should insert a discretionary space.
if st.leadWord != "" && tk.trimRight == tk then
if st.leadWord != "" && tk.trimAsciiEnd == tk.toSlice then
let insertSpace ← do
if ident && st.leadWordIdent then
-- Both idents => need space
pure true
else
-- Check if we would parse more than `tk` as a single token
let tk' := tk.trimLeft
let tk' := tk.trimAsciiStart.copy
let t ← parseToken $ tk' ++ st.leadWord
if t.pos ≤ tk'.rawEndPos then
-- stopped within `tk` => use it as is
@ -398,21 +398,21 @@ def pushToken (info : SourceInfo) (tk : String) (ident : Bool) : FormatterM Unit
if !insertSpace then
-- extend `leadWord` if not prefixed by whitespace
push tk
modify fun st => { st with leadWord := if tk.trimLeft == tk then tk ++ st.leadWord else "", leadWordIdent := ident }
modify fun st => { st with leadWord := if tk.trimAsciiStart == tk.toSlice then tk ++ st.leadWord else "", leadWordIdent := ident }
else
pushLine
push tk
modify fun st => { st with leadWord := if tk.trimLeft == tk then tk else "", leadWordIdent := ident }
modify fun st => { st with leadWord := if tk.trimAsciiStart == tk.toSlice then tk else "", leadWordIdent := ident }
else
-- already separated => use `tk` as is
if st.leadWord == "" then
push tk.trimRight
push tk.trimAsciiEnd.copy
else if tk.endsWith " " then
pushLine
push tk.trimRight
push tk.trimAsciiEnd.copy
else
push tk -- preserve special whitespace for tokens like ":=\n"
modify fun st => { st with leadWord := if tk.trimLeft == tk then tk else "", leadWordIdent := ident }
modify fun st => { st with leadWord := if tk.trimAsciiStart == tk.toSlice then tk else "", leadWordIdent := ident }
if let SourceInfo.original ss _ _ _ := info then
-- preserve non-whitespace content (comments)
@ -613,7 +613,7 @@ instance : Std.Format.MonadPrettyFormat M where
pushOutput s := do
let lineEnd := s.find (· == '\n')
if lineEnd < s.rawEndPos then
let s := (String.Pos.Raw.extract s 0 lineEnd).trimRight ++ continuation
let s := (String.Pos.Raw.extract s 0 lineEnd).trimAsciiEnd.copy ++ continuation
modify fun st => { st with line := st.line.append s }
throw ()
else

View file

@ -96,7 +96,7 @@ def collectAvailableImportsFromLake : IO (Option AvailableImports) := do
args := #["available-imports"]
}
let lakeProc ← IO.Process.spawn spawnArgs
let stdout := String.trim (← lakeProc.stdout.readToEnd)
let stdout := String.trimAscii (← lakeProc.stdout.readToEnd) |>.copy
let exitCode ← lakeProc.wait
match exitCode with
| 0 =>

View file

@ -88,11 +88,11 @@ def rewriteExamples (docstring : String) : String := Id.run do
-- The current state, which tracks the context of the line being processed
let mut inOutput : RWState := .normal
for l in lines do
let indent := l.takeWhile (· == ' ') |>.length
let mut l' := l.trimLeft
let indent := l.takeWhile (· == ' ') |>.copy |>.length
let mut l' := l.trimAsciiStart
-- Is this a code block fence?
if l'.startsWith "```" then
let count := l'.takeWhile (· == '`') |>.length
let count := l'.takeWhile (· == '`') |>.copy |>.length
l' := l'.dropWhile (· == '`')
l' := l'.dropWhile (· == ' ')
match inOutput with

View file

@ -51,7 +51,7 @@ partial def runLakeSetupFile
processStderr (acc ++ line)
let stderr ← ServerTask.IO.asTask (processStderr "")
let stdout := String.trim (← lakeProc.stdout.readToEnd)
let stdout := String.trimAscii (← lakeProc.stdout.readToEnd) |>.copy
let stderr ← IO.ofExcept stderr.get
let exitCode ← lakeProc.wait
return ⟨spawnArgs, exitCode, stdout, stderr⟩

View file

@ -636,13 +636,13 @@ def processGenericRequest : RunnerM Unit := do
logResponse s.method params
def processDirective (ws directive : String) (directiveTargetLineNo : Nat) : RunnerM Unit := do
let directive := directive.drop 1
let directive := directive.drop 1 |>.copy
let colon := directive.posOf ':'
let method := String.Pos.Raw.extract directive 0 colon |>.trim
let method := String.Pos.Raw.extract directive 0 colon |>.trimAscii |>.copy
-- TODO: correctly compute in presence of Unicode
let directiveTargetColumn := ws.rawEndPos + "--"
let pos : Lsp.Position := { line := directiveTargetLineNo, character := directiveTargetColumn.byteIdx }
let params := if colon < directive.rawEndPos then String.Pos.Raw.extract directive (colon + ':') directive.rawEndPos |>.trim else "{}"
let params := if colon < directive.rawEndPos then String.Pos.Raw.extract directive (colon + ':') directive.rawEndPos |>.trimAscii |>.copy else "{}"
modify fun s => { s with pos, method, params }
match method with
-- `delete: "foo"` deletes the given string's number of characters at the given position.

View file

@ -254,8 +254,8 @@ def shellMain
let contents ←
if contents.startsWith "#lang" then
let endLinePos := contents.posOf '\n'
let langId := String.Pos.Raw.extract contents ⟨6⟩ endLinePos |>.trim
if langId == "lean4" then
let langId := String.Pos.Raw.extract contents ⟨6⟩ endLinePos |>.trimAscii
if langId == "lean4".toSlice then
pure () -- do nothing for now
else
IO.eprintln s!"unknown language '{langId}'\n";

View file

@ -150,7 +150,7 @@ def moduleNameOfFileName (fname : FilePath) (rootDir : Option FilePath) : IO Nam
throw $ IO.userError s!"input file '{fname}' must be contained in root directory ({rootDir})"
-- NOTE: use `fname` instead of `fname.normalize` to preserve casing on all platforms
let fnameSuffix := fname.toString.drop rootDir.toString.length
let modNameStr := FilePath.mk fnameSuffix |>.withExtension ""
let modNameStr := FilePath.mk fnameSuffix.copy |>.withExtension ""
let modName := modNameStr.components.foldl Name.mkStr Name.anonymous
pure modName
@ -179,6 +179,6 @@ def findSysroot (lean := "lean") : IO FilePath := do
cmd := lean
args := #["--print-prefix"]
}
return out.trim
return out.trimAscii.copy
end Lean

View file

@ -627,7 +627,7 @@ private def pad (size : Nat) (n : Int) (cut : Bool := false) : String :=
let numStr := toString n
if numStr.length > size then
sign ++ if cut then numStr.drop (numStr.length - size) else numStr
sign ++ if cut then numStr.drop (numStr.length - size) |>.copy else numStr
else
sign ++ leftPad size '0' numStr
@ -636,7 +636,7 @@ private def rightTruncate (size : Nat) (n : Int) (cut : Bool := false) : String
let numStr := toString n
if numStr.length > size then
sign ++ if cut then numStr.take size else numStr
sign ++ if cut then numStr.take size |>.copy else numStr
else
sign ++ rightPad size '0' numStr

View file

@ -66,8 +66,8 @@ def idFromPath (path : System.FilePath) : Option String := do
let last₁ ← res[res.size - 2]?
if last₁ = "zoneinfo"
then some <| last.trim
else some <| last₁.trim ++ "/" ++ last.trim
then some <| last.trimAscii.copy
else some <| last₁.trimAscii.copy ++ "/" ++ last.trimAscii
/--
Retrieves the timezone rules from the local timezone data file.

View file

@ -70,7 +70,7 @@ public def compileLeanModule
unless txt.isEmpty do
logInfo s!"stdout:\n{txt}"
unless out.stderr.isEmpty do
logInfo s!"stderr:\n{out.stderr.trim}"
logInfo s!"stderr:\n{out.stderr.trimAscii}"
if out.exitCode ≠ 0 then
error s!"Lean exited with code {out.exitCode}"

View file

@ -68,7 +68,7 @@ def computeDynlibOfShared (sharedLibTarget : Job FilePath) : SpawnM (Job Dynlib)
if Platform.isWindows then
return {path := sharedLib, name := stem}
else if stem.startsWith "lib" then
return {path := sharedLib, name := stem.drop 3}
return {path := sharedLib, name := stem.drop 3 |>.copy}
else
error s!"shared library `{sharedLib}` does not start with `lib`; this is not supported on Unix"
else

View file

@ -74,13 +74,13 @@ where
if pkg.isEmpty then
return .package .anonymous
else
return .package (stringToLegalOrSimpleName pkg)
return .package (stringToLegalOrSimpleName pkg.copy)
else if target.startsWith "+" then
return .module (stringToLegalOrSimpleName (target.drop 1))
return .module (stringToLegalOrSimpleName (target.drop 1).copy)
else
parsePackageTarget .anonymous target
| [pkg, target] =>
let pkg := if pkg.startsWith "@" then pkg.drop 1 else pkg
let pkg := if pkg.startsWith "@" then pkg.drop 1 |>.copy else pkg
if pkg.isEmpty then
parsePackageTarget .anonymous target
else
@ -91,7 +91,7 @@ where
if target.isEmpty then
throw s!"ill-formed target: default package targets are not supported in partial build keys"
else if target.startsWith "+" then
let target := target.drop 1 |> stringToLegalOrSimpleName
let target := target.drop 1 |>.copy |> stringToLegalOrSimpleName
return .packageModule pkg target
else
let target := stringToLegalOrSimpleName target

View file

@ -210,7 +210,7 @@ private def resolveTargetBaseSpec
: EIO CliError (Array BuildSpec) := do
if spec.startsWith "@" then
let spec := spec.drop 1
resolveTargetLikeSpec ws spec facet (explicit := true)
resolveTargetLikeSpec ws spec.copy facet (explicit := true)
else if spec.startsWith "+" then
let mod := spec.drop 1 |>.toName
if let some mod := ws.findTargetModule? mod then
@ -237,7 +237,7 @@ public def parseExeTargetSpec
| some exe => return exe
| none => throw <| CliError.unknownExe spec
| [pkgSpec, targetSpec] =>
let pkgSpec := if pkgSpec.startsWith "@" then pkgSpec.drop 1 else pkgSpec
let pkgSpec := if pkgSpec.startsWith "@" then pkgSpec.drop 1 |>.copy else pkgSpec
let pkg ← parsePackageSpec ws pkgSpec
let targetName := stringToLegalOrSimpleName targetSpec
match pkg.findLeanExe? targetName with

View file

@ -525,7 +525,7 @@ public def init
| none => error s!"illegal package name: could not derive one from '{path}'"
else
return name
let name := name.trim
let name := name.trimAscii.copy
validatePkgName name
IO.FS.createDirAll cwd
initPkg cwd (stringToLegalOrSimpleName name) tmp lang env offline
@ -534,7 +534,7 @@ public def new
(name : String) (tmp : InitTemplate) (lang : ConfigLang)
(env : Lake.Env) (cwd : FilePath := ".") (offline := false)
: LoggerIO PUnit := do
let name := name.trim
let name := name.trimAscii.copy
validatePkgName name
let name := stringToLegalOrSimpleName name
let dirName := dotlessName name

View file

@ -32,6 +32,6 @@ public def Package.mkConfigString (pkg : Package) (lang : ConfigLang) : LogIO St
let env ← importModulesUsingCache #[`Lake] {} 1024
let pp := ppModule <| descopeTSyntax <| pkg.mkLeanConfig
match (← pp.toIO {fileName := "", fileMap := default} {env} |>.toBaseIO) with
| .ok (fmt, _) => pure <| (toString fmt).trim ++ "\n"
| .ok (fmt, _) => pure <| (toString fmt).trimAscii.copy ++ "\n"
| .error ex =>
error s!"(internal) failed to pretty print Lean configuration: {ex.toString}"

View file

@ -265,7 +265,7 @@ def Dependency.mkRequire (cfg : Dependency) : RequireDecl := Unhygienic.run do
let ver? ←
if let some ver := cfg.version? then
if ver.startsWith "git#" then
some <$> `(verSpec|git $(toLean <| ver.drop 4))
some <$> `(verSpec|git $(toLean <| ver.drop 4 |>.copy))
else
some <$> `(verSpec|$(toLean ver):term)
else

View file

@ -48,7 +48,7 @@ public partial def parse (inputName : String) (contents : String) : LoggerIO Cac
let rec loop (i : Nat) (cache : CacheMap) (stopPos pos : String.Pos.Raw) := do
let lfPos := contents.posOfAux '\n' stopPos pos
let line := String.Pos.Raw.extract contents pos lfPos
if line.trim.isEmpty then
if line.trimAscii.isEmpty then
return cache
let cache ← id do
match Json.parse line >>= fromJson? with
@ -63,7 +63,7 @@ public partial def parse (inputName : String) (contents : String) : LoggerIO Cac
loop (i+1) cache stopPos (lfPos.next' contents h)
let lfPos := contents.posOfAux '\n' contents.rawEndPos 0
let line := String.Pos.Raw.extract contents 0 lfPos
checkSchemaVersion inputName line.trim
checkSchemaVersion inputName line.trimAscii.copy
if h : lfPos.atEnd contents then
return {}
else

View file

@ -159,7 +159,7 @@ public def compute
reservoirApiUrl := ← getUrlD "RESERVOIR_API_URL" s!"{reservoirBaseUrl}/v1"
noCache := (noCache <|> (← IO.getEnv "LAKE_NO_CACHE").bind envToBool?).getD false
enableArtifactCache := (← IO.getEnv "LAKE_ARTIFACT_CACHE").bind envToBool? |>.getD false
cacheKey? := (← IO.getEnv "LAKE_CACHE_KEY").map (·.trim)
cacheKey? := (← IO.getEnv "LAKE_CACHE_KEY").map (·.trimAscii.copy)
cacheArtifactEndpoint? := (← IO.getEnv "LAKE_CACHE_ARTIFACT_ENDPOINT").map normalizeUrl
cacheRevisionEndpoint? := (← IO.getEnv "LAKE_CACHE_REVISION_ENDPOINT").map normalizeUrl
githashOverride := (← IO.getEnv "LEAN_GITHASH").getD ""
@ -193,7 +193,7 @@ where
else
return default
normalizeUrl url :=
if url.back == '/' then url.dropRight 1 else url
if url.back == '/' then url.dropEnd 1 |>.copy else url
/--
The string Lake uses to identify Lean in traces.

View file

@ -166,7 +166,7 @@ environment variables. If `ELAN` is set but empty, Elan is considered disabled.
public def findElanInstall? : BaseIO (Option ElanInstall) := do
if let some home ← IO.getEnv "ELAN_HOME" then
let elan := (← IO.getEnv "ELAN").getD "elan"
if elan.trim.isEmpty then
if elan.trimAscii.isEmpty then
return none
else
return some {elan, home}
@ -184,7 +184,7 @@ public def findLeanSysroot? (lean := "lean") : BaseIO (Option FilePath) := do
args := #["--print-prefix"]
}
if out.exitCode == 0 then
pure <| some <| FilePath.mk <| out.stdout.trim
pure <| some <| FilePath.mk <| out.stdout.trimAscii.copy
else
pure <| none
act.catchExceptions fun _ => pure none
@ -234,7 +234,7 @@ where
cmd := leanExe sysroot |>.toString,
args := #["--githash"]
}
return out.stdout.trim
return out.stdout.trimAscii.copy
findAr := do
if let some ar ← IO.getEnv "LEAN_AR" then
return FilePath.mk ar
@ -322,7 +322,7 @@ public def findLeanInstall? : BaseIO (Option LeanInstall) := do
return some <| ← LeanInstall.get sysroot
let lean ← do
if let some lean ← IO.getEnv "LEAN" then
if lean.trim.isEmpty then
if lean.trimAscii.isEmpty then
return none
else
pure lean

View file

@ -26,8 +26,8 @@ export ToText (toText)
public instance (priority := 0) [ToString α] : ToText α := ⟨toString⟩
public instance : ToText Json := ⟨Json.compress⟩
public instance [ToText α] : ToText (List α) := ⟨(·.foldl (s!"{·}{toText ·}\n") "" |>.dropRight 1)⟩
public instance [ToText α] : ToText (Array α) := ⟨(·.foldl (s!"{·}{toText ·}\n") "" |>.dropRight 1)⟩
public instance [ToText α] : ToText (List α) := ⟨(·.foldl (s!"{·}{toText ·}\n") "" |>.dropEnd 1 |>.copy)⟩
public instance [ToText α] : ToText (Array α) := ⟨(·.foldl (s!"{·}{toText ·}\n") "" |>.dropEnd 1 |>.copy)⟩
/-- Class used to format target output as text for `lake query`. -/
public class QueryText (α : Type u) where

View file

@ -147,12 +147,12 @@ public def Reservoir.fetchPkg? (lakeEnv : Lake.Env) (owner pkg : String) : LogIO
| .error e =>
errorWithLog do
logError s!"{owner}/{pkg}: Reservoir lookup failed; server returned unsupported JSON: {e}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trim}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trimAscii}"
failure
| .error e =>
errorWithLog do
logError s!"{owner}/{pkg}: Reservoir lookup failed; server returned invalid JSON: {e}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trim}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trimAscii}"
failure
/--
@ -199,10 +199,10 @@ public def Reservoir.fetchPkgVersions
| .error e =>
errorWithLog do
logError s!"{owner}/{pkg}: Reservoir lookup failed; server returned unsupported JSON: {e}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trim}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trimAscii}"
failure
| .error e =>
errorWithLog do
logError s!"{owner}/{pkg}: Reservoir lookup failed; server returned invalid JSON: {e}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trim}"
logVerbose s!"{owner}/{pkg}: Reservoir responded with:\n{out.trimAscii}"
failure

View file

@ -105,7 +105,7 @@ public def ofString? (dt : String) : Option DateTime := do
| [d,t] =>
let d ← Date.ofString? d
if t.back == 'Z' || t.back == 'z' then
return offsetDateTime d (← Time.ofString? <| t.dropRight 1)
return offsetDateTime d (← Time.ofString? <| t.dropEnd 1 |>.copy)
else if let [t,o] := t.splitToList (· == '+') then
return offsetDateTime d (← Time.ofString? t) <| some (false, ← Time.ofString? o)
else if let [t,o] := t.splitToList (· == '-') then

View file

@ -133,7 +133,7 @@ public def ppTable (t : Table) : String :=
| _ => (appendKeyval ts k v, fs)
-- Ensures root table keys come before subtables
-- See https://github.com/leanprover/lean4/issues/4099
(ts.push '\n' ++ fs).trimRight.push '\n'
(ts.push '\n' ++ fs).trimAsciiEnd.copy.push '\n'
where
appendKeyval s k v :=
s.append s!"{ppKey k} = {v}\n"

View file

@ -42,9 +42,9 @@ def decodeDecNum (s : String) : Nat :=
def decodeSign (s : String) : Bool × String :=
if s.front == '-' then
(true, s.drop 1)
(true, s.drop 1 |>.copy)
else if s.front == '+' then
(false, s.drop 1)
(false, s.drop 1 |>.copy)
else
(false, s)
@ -122,7 +122,7 @@ def elabDateTime (x : TSyntax ``dateTime) : CoreM DateTime := do
--------------------------------------------------------------------------------
def elabLiteralString (x : TSyntax ``literalString) : CoreM String := do
return (← elabLit x "literalString").drop 1 |>.dropRight 1
return (← elabLit x "literalString").drop 1 |>.dropEnd 1 |>.copy
def decodeHexDigits (s : Substring.Raw) : Nat :=
s.foldl (init := 0) fun n c => n*16 + decodeHexDigit c
@ -164,23 +164,23 @@ partial def elabBasicStringCore (lit : String) (i : String.Pos.Raw := 0) (out :=
def elabBasicString (x : TSyntax ``basicString) : CoreM String := do
let spelling ← elabLit x "basic string"
withRef x <| elabBasicStringCore (spelling.drop 1 |>.dropRight 1)
withRef x <| elabBasicStringCore (spelling.drop 1 |>.dropEnd 1 |>.copy)
def dropInitialNewline (s : String) : String :=
if s.front == '\r' then
s.drop 2
s.drop 2 |>.copy
else if s.front == '\n' then
s.drop 1
s.drop 1 |>.copy
else
s
def elabMlLiteralString (x : TSyntax ``mlLiteralString) : CoreM String := do
let spelling ← elabLit x "multi-line literal string"
return dropInitialNewline (spelling.drop 3 |>.dropRight 3)
return dropInitialNewline (spelling.drop 3 |>.dropEnd 3 |>.copy)
def elabMlBasicString (x : TSyntax ``mlBasicString) : CoreM String := do
let spelling ← elabLit x "multi-line basic string"
withRef x <| elabBasicStringCore (dropInitialNewline (spelling.drop 3 |>.dropRight 3))
withRef x <| elabBasicStringCore (dropInitialNewline (spelling.drop 3 |>.dropEnd 3 |>.copy))
def elabString (x : TSyntax ``string) : CoreM String := do
match x with

View file

@ -173,7 +173,7 @@ public def chAtom.parenthesizer (_ : Char) (_ : List String) (_ : ParserFn) : P
/-- Parse the trimmed string as an atom (but use the full string for formatting). -/
public def strAtom (s : String) (expected := [s!"'{s}'"]) (trailingFn := skipFn) : Parser :=
atom (strFn s.trim expected) trailingFn
atom (strFn s.trimAscii.copy expected) trailingFn
@[combinator_formatter strAtom]
public def strAtom.formatter (s : String) (_ : List String) (_ : ParserFn) : Formatter :=

View file

@ -8,6 +8,7 @@ module
prelude
import Init.Data.Array.Basic
public import Init.Data.String.TakeDrop
import Init.Data.String.Slice
namespace Lake
@ -69,15 +70,15 @@ variable [Monad m] [MonadStateOf ArgList m]
/-- Process a short option of the form `-x=arg`. -/
@[inline] public def shortOptionWithEq (handle : Char → m α) (opt : String) : m α := do
consArg (opt.drop 3); handle (String.Pos.Raw.get opt ⟨1⟩)
consArg (opt.drop 3).copy; handle (String.Pos.Raw.get opt ⟨1⟩)
/-- Process a short option of the form `"-x arg"`. -/
@[inline] public def shortOptionWithSpace (handle : Char → m α) (opt : String) : m α := do
consArg <| opt.drop 2 |>.trimLeft; handle (String.Pos.Raw.get opt ⟨1⟩)
consArg <| opt.drop 2 |>.trimAsciiStart |>.copy; handle (String.Pos.Raw.get opt ⟨1⟩)
/-- Process a short option of the form `-xarg`. -/
@[inline] public def shortOptionWithArg (handle : Char → m α) (opt : String) : m α := do
consArg (opt.drop 2); handle (String.Pos.Raw.get opt ⟨1⟩)
consArg (opt.drop 2).copy; handle (String.Pos.Raw.get opt ⟨1⟩)
/-- Process a multiple short options grouped together (ex. `-xyz` as `x`, `y`, `z`). -/
@[inline] public def multiShortOption (handle : Char → m PUnit) (opt : String) : m PUnit := do

View file

@ -64,8 +64,8 @@ Examples:
-/
public def modOfFilePath (path : FilePath) : Name :=
let path := removeExts path.normalize.toString
let path := path.stripSuffix FilePath.pathSeparator.toString
FilePath.components path |>.foldl .str .anonymous
let path := path.dropSuffix FilePath.pathSeparator.toString
FilePath.components path.copy |>.foldl .str .anonymous
where
removeExts (s : String) (i := s.rawEndPos) (e := s.rawEndPos) :=
if h : i = 0 then

View file

@ -30,7 +30,7 @@ public def filterUrl? (url : String) : Option String :=
if url.startsWith "git" then
none
else if url.endsWith ".git" then
some <| url.dropRight 4
some <| url.dropEnd 4 |>.copy
else
some url

View file

@ -155,8 +155,8 @@ public instance : ToString LogEntry := ⟨LogEntry.toString⟩
{level := .error, message}
public def LogEntry.ofSerialMessage (msg : SerialMessage) : LogEntry :=
let str := if msg.caption.trim.isEmpty then
msg.data.trim else s!"{msg.caption.trim}:\n{msg.data.trim}"
let str := if msg.caption.trimAscii.isEmpty then
msg.data.trimAscii.copy else s!"{msg.caption.trimAscii}:\n{msg.data.trimAscii}"
{
level := .ofMessageSeverity msg.severity
message := mkErrorStringWithPos msg.fileName msg.pos str none
@ -446,7 +446,7 @@ from an `ELogT` (e.g., `LogIO`).
let buf ← liftM (m := BaseIO) buf.get
let out := String.fromUTF8! buf.data
unless out.isEmpty do
logInfo s!"stdout/stderr:\n{out.trim}"
logInfo s!"stdout/stderr:\n{out.trimAscii}"
return a
/-- Throw with the logged error `message`. -/

View file

@ -22,9 +22,9 @@ public def mkCmdLog (args : IO.Process.SpawnArgs) : String :=
[Monad m] (out : IO.Process.Output) (log : String → m PUnit)
: m Unit := do
unless out.stdout.isEmpty do
log s!"stdout:\n{out.stdout.trim}"
log s!"stdout:\n{out.stdout.trimAscii}"
unless out.stderr.isEmpty do
log s!"stderr:\n{out.stderr.trim}"
log s!"stderr:\n{out.stderr.trimAscii}"
@[inline] public def rawProc (args : IO.Process.SpawnArgs) (quiet := false) : LogIO IO.Process.Output := do
withLogErrorPos do
@ -50,13 +50,13 @@ public def captureProc' (args : IO.Process.SpawnArgs) : LogIO (IO.Process.Output
logError s!"external command '{args.cmd}' exited with code {out.exitCode}"
@[inline] public def captureProc (args : IO.Process.SpawnArgs) : LogIO String := do
return (← captureProc' args).stdout.trim -- remove, e.g., newline at end
return (← captureProc' args).stdout.trimAscii.copy -- remove, e.g., newline at end
public def captureProc? (args : IO.Process.SpawnArgs) : BaseIO (Option String) := do
EIO.catchExceptions (h := fun _ => pure none) do
let out ← IO.Process.output args
if out.exitCode = 0 then
return some out.stdout.trim -- remove, e.g., newline at end
return some out.stdout.trimAscii.copy -- remove, e.g., newline at end
else
return none

View file

@ -120,7 +120,7 @@ public def getUrl?
| .ok none => error s!"curl's JSON output did not contain a response code"
| .error e => error s!"curl's JSON output contained an invalid JSON response code: {e}"
if code == 200 then
return some out.stdout.trim
return some out.stdout.trimAscii.copy
else if code == 404 then
return none
else

View file

@ -260,7 +260,7 @@ public def ofString (ver : String) : ToolchainVer := Id.run do
("", ver)
let noOrigin := origin.isEmpty
if tag.startsWith "v" then
if let .ok ver := StdVer.parse (tag.drop 1) then
if let .ok ver := StdVer.parse (tag.drop 1).copy then
if noOrigin|| origin == defaultOrigin then
return .release ver
else if let some date := tag.dropPrefix? "nightly-" then
@ -268,7 +268,7 @@ public def ofString (ver : String) : ToolchainVer := Id.run do
if noOrigin then
return .nightly date
else if let some suffix := origin.dropPrefix? defaultOrigin then
if suffix.isEmpty || suffix == "-nightly".toRawSubstring then
if suffix.isEmpty || suffix == "-nightly".toSlice then
return .nightly date
else if let some n := tag.dropPrefix? "pr-release-" then
if let some n := n.toNat? then
@ -283,7 +283,7 @@ public def ofString (ver : String) : ToolchainVer := Id.run do
public def ofFile? (toolchainFile : FilePath) : IO (Option ToolchainVer) := do
try
let toolchainString ← IO.FS.readFile toolchainFile
return some <| ToolchainVer.ofString toolchainString.trim
return some <| ToolchainVer.ofString toolchainString.trimAscii.copy
catch
| .noFileOrDirectory .. =>
return none

View file

@ -50,7 +50,7 @@ def Array.findPrefix : Array String → String → Array String := fun a s =>
/-- The intended semanics of `Trie.matchPrefix`: Longest prefix found in trie -/
def Array.matchPrefix : Array String → String → Option String := fun a s => Id.run do
for i in List.reverse (List.range (s.length + 1)) do
let pfix := s.take i
let pfix := s.take i |>.copy
if let some _ := a.find? (· == pfix) then
return some pfix
return none

View file

@ -27,7 +27,7 @@ def testShouldEscape :=
(Char.ofNat 127).toString] -- for 0x7F
assert! should_quote.data.all (λ c =>
let x := (escapeUri c.toString)
x.length == 3 && x.take 1 == "%")
x.length == 3 && x.take 1 == "%".toSlice)
true
def testPartialEscape :=

View file

@ -106,7 +106,7 @@ def main : List String → IO UInt32
| IO.eprintln "Expected file in current directory"
return 4
let kind := file.takeWhile (· != '_')
let some p := testConfigs.lookup kind
let some p := testConfigs.lookup kind.copy
| IO.eprintln s!"Not found in test configs: {kind}"
return 5
IO.print <| ← test p (← IO.FS.readFile inputFile)

View file

@ -120,7 +120,7 @@ def withReportedOutput (x : MetaM α) : MetaM Unit := do
-- We need to omit the path to the file, since that's host-dependent; also drop line
-- number to avoid noise
let dropped := res.splitOn fileName |>.map (fun (s : String) => s.dropWhile (· != ' '))
logInfo ("".intercalate dropped)
logInfo ("".toSlice.intercalate dropped)
/--
info: error(lean.bar): function is noncomputable

View file

@ -39,7 +39,7 @@ def deletions (n : Nat) (s : String) : Array String :=
for s' in deletions n' s do
if s'.isEmpty then break
for i in [0:s'.length] do
let d := s'.take i ++ s'.drop (i + 1)
let d := (s'.take i).copy ++ s'.drop (i + 1)
if !out.contains d then out := out.push d
return out.reverse
@ -92,7 +92,7 @@ def insertions (toInsert : String) (s : String) : Array String := Id.run do
let mut next := #[]
for s' in out do
for i in [0:s'.length + 1] do
next := next.push ((s'.take i).push c ++ s'.drop i)
next := next.push ((s'.take i |>.copy).push c ++ s'.drop i)
out := next
return out

View file

@ -151,7 +151,7 @@ def String.dedent (s : String) : Option String :=
if !parts.all (·.startsWith "|") then
none
else
p₀ ++ "\n" ++ String.intercalate "\n" (parts.map fun p => p.drop 1)
p₀ ++ "\n" ++ String.intercalate "\n" (parts.map fun p => p.drop 1 |>.copy)
elab "d!" s:str : term => do
let some s := s.raw.isStrLit? | Lean.Elab.throwIllFormedSyntax

View file

@ -46,7 +46,7 @@ it₁.remainingToString ++ "-" ++ it₂.remainingToString
#eval "αβ".mkIterator.next.prev.hasPrev
#eval "abc" == "abc"
#eval "abc" == "abd"
#eval "αβγ".drop 1
#eval "αβγ".drop 1 |>.copy
#eval "αβγ".takeRight 1
def ss : Substring.Raw := "0123abcdαβγδ".toRawSubstring