lean4-htt/src/Init/Data/Format/Basic.lean
2026-02-05 09:10:32 +00:00

472 lines
16 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2018 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Leonardo de Moura
-/
module
prelude
public import Init.Data.Int.Basic
public import Init.Data.String.Bootstrap
import Init.Control.State
import Init.Data.Nat.Bitwise.Basic
public section
namespace Std
/--
Determines how groups should have linebreaks inserted when the text would overfill its remaining
space.
- `allOrNone` will make a linebreak on every `Format.line` in the group or none of them.
```
[1,
2,
3]
```
- `fill` will only make linebreaks on as few `Format.line`s as possible:
```
[1, 2,
3]
```
-/
inductive Format.FlattenBehavior where
/--
Either all `Format.line`s in the group will be newlines, or all of them will be spaces.
-/
| allOrNone
/--
As few `Format.line`s in the group as possible will be newlines.
-/
| fill
deriving Inhabited, BEq
open Format in
/--
A representation of a set of strings, in which the placement of newlines and indentation differ.
Given a specific line width, specified in columns, the string that uses the fewest lines can be
selected.
The pretty-printing algorithm is based on Wadler's paper
[_A Prettier Printer_](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf).
-/
inductive Format where
/-- The empty format. -/
| nil : Format
/--
A position where a newline may be inserted if the current group does not fit within the allotted
column width.
-/
| line : Format
/--
`align` tells the formatter to pad with spaces to the current indentation level, or else add a
newline if we are already at or past the indent.
If `force` is true, then it will pad to the indent even if it is in a flattened group.
Example:
```lean example
open Std Format in
#eval IO.println (nest 2 <| "." ++ align ++ "a" ++ line ++ "b")
```
```lean output
. a
b
```
-/
| align (force : Bool) : Format
/--
A node containing a plain string.
If the string contains newlines, the formatter emits them and then indents to the current level.
-/
| text : String → Format
/--
`nest indent f` increases the current indentation level by `indent` while rendering `f`.
Example:
```lean example
open Std Format in
def fmtList (l : List Format) : Format :=
let f := joinSep l (", " ++ Format.line)
group (nest 1 <| "[" ++ f ++ "]")
```
This will be written all on one line, but if the text is too large, the formatter will put in
linebreaks after the commas and indent later lines by 1.
-/
| nest (indent : Int) (f : Format) : Format
/-- Concatenation of two `Format`s. -/
| append : Format → Format → Format
/-- Creates a new flattening group for the given inner `Format`. -/
| group : Format → (behavior : FlattenBehavior := FlattenBehavior.allOrNone) → Format
/-- Used for associating auxiliary information (e.g. `Expr`s) with `Format` objects. -/
| tag : Nat → Format → Format
deriving Inhabited
namespace Format
/-- Checks whether the given format contains no characters. -/
def isEmpty : Format → Bool
| nil => true
| line => false
| align _ => true
| text msg => msg == ""
| nest _ f => f.isEmpty
| append f₁ f₂ => f₁.isEmpty && f₂.isEmpty
| group f _ => f.isEmpty
| tag _ f => f.isEmpty
/--
Creates a group in which as few `Format.line`s as possible are rendered as newlines.
This is an alias for `Format.group`, with `FlattenBehavior` set to `fill`.
-/
def fill (f : Format) : Format :=
group f (behavior := FlattenBehavior.fill)
instance : Append Format := ⟨Format.append⟩
instance : Coe String Format := ⟨text⟩
/--
Concatenates a list of `Format`s with `++`.
-/
def join (xs : List Format) : Format :=
xs.foldl (·++·) ""
/--
Checks whether a `Format` is the constructor `Format.nil`.
This does not check whether the resulting rendered strings are always empty. To do that, use
`Format.isEmpty`.
-/
def isNil : Format → Bool
| nil => true
| _ => false
private structure SpaceResult where
foundLine : Bool := false
foundFlattenedHardLine : Bool := false
space : Nat := 0
deriving Inhabited
@[inline] private def merge (w : Nat) (r₁ : SpaceResult) (r₂ : Nat → SpaceResult) : SpaceResult :=
if r₁.space > w || r₁.foundLine then
r₁
else
let r₂ := r₂ (w - r₁.space);
{ r₂ with space := r₁.space + r₂.space }
private def spaceUptoLine : Format → Bool → Int → Nat → SpaceResult
| nil, _, _, _ => {}
| line, flatten, _, _ => if flatten then { space := 1 } else { foundLine := true }
| align force, flatten, m, w =>
if flatten && !force then {}
else if w < m then
{ space := (m - w).toNat }
else
{ foundLine := true }
| text s, flatten, _, _ =>
let p := String.Internal.posOf s '\n'
let off := String.Internal.offsetOfPos s p
{ foundLine := p != s.rawEndPos, foundFlattenedHardLine := flatten && p != s.rawEndPos, space := off }
| append f₁ f₂, flatten, m, w => merge w (spaceUptoLine f₁ flatten m w) (spaceUptoLine f₂ flatten m)
| nest n f, flatten, m, w => spaceUptoLine f flatten (m - n) w
| group f _, _, m, w => spaceUptoLine f true m w
| tag _ f, flatten, m, w => spaceUptoLine f flatten m w
private structure WorkItem where
f : Format
indent : Int
activeTags : Nat
/--
A directive indicating whether a given work group is able to be flattened.
- `allow` indicates that the group is allowed to be flattened; its argument is `true` if
there is sufficient space for it to be flattened (and so it should be), or `false` if not.
- `disallow` means that this group should not be flattened irrespective of space concerns.
This is used at levels of a `Format` outside of any flattening groups. It is necessary to track
this so that, after a hard line break, we know whether to try to flatten the next line.
-/
inductive FlattenAllowability where
| allow (fits : Bool)
| disallow
deriving BEq
/-- Whether the given directive indicates that flattening should occur. -/
def FlattenAllowability.shouldFlatten : FlattenAllowability → Bool
| allow true => true
| _ => false
private structure WorkGroup where
fla : FlattenAllowability
flb : FlattenBehavior
items : List WorkItem
private partial def spaceUptoLine' : List WorkGroup → Nat → Nat → SpaceResult
| [], _, _ => {}
| { items := [], .. }::gs, col, w => spaceUptoLine' gs col w
| g@{ items := i::is, .. }::gs, col, w =>
merge w
(spaceUptoLine i.f g.fla.shouldFlatten (w + col - i.indent) w)
(spaceUptoLine' ({ g with items := is }::gs) col)
/--
A monad that can be used to incrementally render `Format` objects.
-/
class MonadPrettyFormat (m : Type → Type) where
/--
Emits the string `s`.
-/
pushOutput (s : String) : m Unit
/--
Emits a newline followed by `indent` columns of indentation.
-/
pushNewline (indent : Nat) : m Unit
/--
Gets the current column at which the next string will be emitted.
-/
currColumn : m Nat
/--
Starts a region tagged with `tag`.
-/
startTag (tag : Nat) : m Unit
/--
Exits the scope of `count` opened tags.
-/
endTags (count : Nat) : m Unit
open MonadPrettyFormat
private def pushGroup (flb : FlattenBehavior) (items : List WorkItem) (gs : List WorkGroup) (w : Nat) [Monad m] [MonadPrettyFormat m] : m (List WorkGroup) := do
let k ← currColumn
-- Flatten group if it + the remainder (gs) fits in the remaining space. For `fill`, measure only up to the next (ungrouped) line break.
let g := { fla := .allow (flb == FlattenBehavior.allOrNone), flb := flb, items := items : WorkGroup }
let r := spaceUptoLine' [g] k (w-k)
let r' := merge (w-k) r (spaceUptoLine' gs k)
-- Prevent flattening if any item contains a hard line break, except within `fill` if it is ungrouped (=> unflattened)
return { g with fla := .allow (!r.foundFlattenedHardLine && r'.space <= w-k) }::gs
private partial def be (w : Nat) [Monad m] [MonadPrettyFormat m] : List WorkGroup → m Unit
| [] => pure ()
| { items := [], .. }::gs => be w gs
| g@{ items := i::is, .. }::gs => do
let gs' (is' : List WorkItem) := { g with items := is' }::gs;
match i.f with
| nil =>
endTags i.activeTags
be w (gs' is)
| tag t f =>
startTag t
be w (gs' ({ i with f, activeTags := i.activeTags + 1 }::is))
| append f₁ f₂ => be w (gs' ({ i with f := f₁, activeTags := 0 }::{ i with f := f₂ }::is))
| nest n f => be w (gs' ({ i with f, indent := i.indent + n }::is))
| text s =>
let p := String.Internal.posOf s '\n'
if p == s.rawEndPos then
pushOutput s
endTags i.activeTags
be w (gs' is)
else
pushOutput (String.Internal.extract s {} p)
pushNewline i.indent.toNat
let is := { i with f := text (String.Internal.extract s (String.Internal.next s p) s.rawEndPos) }::is
-- after a hard line break, re-evaluate whether to flatten the remaining group
-- note that we shouldn't start flattening after a hard break outside a group
if g.fla == .disallow then
be w (gs' is)
else
pushGroup g.flb is gs w >>= be w
| line =>
match g.flb with
| FlattenBehavior.allOrNone =>
if g.fla.shouldFlatten then
-- flatten line = text " "
pushOutput " "
endTags i.activeTags
be w (gs' is)
else
pushNewline i.indent.toNat
endTags i.activeTags
be w (gs' is)
| FlattenBehavior.fill =>
let breakHere := do
pushNewline i.indent.toNat
-- make new `fill` group and recurse
endTags i.activeTags
pushGroup FlattenBehavior.fill is gs w >>= be w
-- if preceding fill item fit in a single line, try to fit next one too
if g.fla.shouldFlatten then
let gs'@(g'::_) ← pushGroup FlattenBehavior.fill is gs (w - String.Internal.length " ")
| panic "unreachable"
if g'.fla.shouldFlatten then
pushOutput " "
endTags i.activeTags
be w gs' -- TODO: use `return`
else
breakHere
else
breakHere
| align force =>
if g.fla.shouldFlatten && !force then
-- flatten (align false) = nil
endTags i.activeTags
be w (gs' is)
else
let k ← currColumn
if k < i.indent then
pushOutput (String.Internal.pushn "" ' ' (i.indent - k).toNat)
endTags i.activeTags
be w (gs' is)
else
pushNewline i.indent.toNat
endTags i.activeTags
be w (gs' is)
| group f flb =>
if g.fla.shouldFlatten then
-- flatten (group f) = flatten f
be w (gs' ({ i with f }::is))
else
pushGroup flb [{ i with f }] (gs' is) w >>= be w
/- Render the given `f : Format` with a line width of `w`.
`indent` is the starting amount to indent each line by. -/
/--
Renders a `Format` using effects in the monad `m`, using the methods of `MonadPrettyFormat`.
Each line is emitted as soon as it is rendered, rather than waiting for the entire document to be
rendered.
* `w`: the total width
* `indent`: the initial indentation to use for wrapped lines (subsequent wrapping may increase the
indentation)
-/
def prettyM (f : Format) (w : Nat) (indent : Nat := 0) [Monad m] [MonadPrettyFormat m] : m Unit :=
be w [{ flb := FlattenBehavior.allOrNone, fla := .disallow, items := [{ f := f, indent, activeTags := 0 }]}]
/--
Creates a format `l ++ f ++ r` with a flattening group, nesting the contents by the length of `l`.
The group's `FlattenBehavior` is `allOrNone`; for `fill` use `Std.Format.bracketFill`.
-/
@[inline] def bracket (l : String) (f : Format) (r : String) : Format :=
group (nest (String.Internal.length l) $ l ++ f ++ r)
/--
Creates the format `"(" ++ f ++ ")"` with a flattening group, nesting by one space.
-/
@[inline] def paren (f : Format) : Format :=
bracket "(" f ")"
/--
Creates the format `"[" ++ f ++ "]"` with a flattening group, nesting by one space.
`sbracket` is short for “square bracket”.
-/
@[inline] def sbracket (f : Format) : Format :=
bracket "[" f "]"
/--
Creates a format `l ++ f ++ r` with a flattening group, nesting the contents by the length of `l`.
The group's `FlattenBehavior` is `fill`; for `allOrNone` use `Std.Format.bracketFill`.
-/
@[inline] def bracketFill (l : String) (f : Format) (r : String) : Format :=
fill (nest (String.Internal.length l) $ l ++ f ++ r)
/-- The default indentation level, which is two spaces. -/
def defIndent := 2
def defUnicode := true
/-- The default width of the targeted output, which is 120 columns. -/
def defWidth := 120
/--
Increases the indentation level by the default amount.
-/
def nestD (f : Format) : Format :=
nest defIndent f
/-- Insert a newline and then `f`, all nested by the default indent amount. -/
def indentD (f : Format) : Format :=
nestD (Format.line ++ f)
/-- State for formatting a pretty string. -/
private structure State where
out : String := ""
column : Nat := 0
private instance : MonadPrettyFormat (StateM State) where
-- We avoid a structure instance update, and write these functions using pattern matching because of issue #316
pushOutput s := modify fun ⟨out, col⟩ => ⟨String.Internal.append out s, col + (String.Internal.length s)⟩
pushNewline indent := modify fun ⟨out, _⟩ => ⟨String.Internal.append out (String.Internal.pushn "\n" ' ' indent), indent⟩
currColumn := return (← get).column
startTag _ := return ()
endTags _ := return ()
/--
Renders a `Format` to a string.
* `width`: the total width
* `indent`: the initial indentation to use for wrapped lines
(subsequent wrapping may increase the indentation)
* `column`: begin the first line wrap `column` characters earlier than usual
(this is useful when the output String will be printed starting at `column`)
-/
def pretty (f : Format) (width : Nat := defWidth) (indent : Nat := 0) (column := 0) : String :=
let act : StateM State Unit := prettyM f width indent
State.out <| act (State.mk "" column) |>.snd
end Format
/--
Specifies a “user-facing” way to convert from the type `α` to a `Format` object. There is no
expectation that the resulting string is valid code.
The `Repr` class is similar, but the expectation is that instances produce valid Lean code.
-/
class ToFormat (α : Type u) where
/--
Converts a value to a `Format` object, with no expectation that the resulting string is valid
code.
-/
format : α → Format
export ToFormat (format)
-- note: must take precedence over the above instance to avoid premature formatting
instance : ToFormat Format where
format f := f
instance : ToFormat String where
format s := Format.text s
/--
Intercalates the given list with the given `sep` format.
The list items are formatting using `ToFormat.format`.
-/
def Format.joinSep {α : Type u} [ToFormat α] : List α → Format → Format
| [], _ => nil
| [a], _ => format a
| a::as, sep => as.foldl (· ++ sep ++ format ·) (format a)
/--
Concatenates the given list after prepending `pre` to each element.
The list items are formatting using `ToFormat.format`.
-/
def Format.prefixJoin {α : Type u} [ToFormat α] (pre : Format) : List α → Format
| [] => nil
| a::as => as.foldl (· ++ pre ++ format ·) (pre ++ format a)
/--
Concatenates the given list after appending the given suffix to each element.
The list items are formatting using `ToFormat.format`.
-/
def Format.joinSuffix {α : Type u} [ToFormat α] : List α → Format → Format
| [], _ => nil
| a::as, suffix => as.foldl (· ++ format · ++ suffix) (format a ++ suffix)
end Std