| /- | |
| Copyright (c) 2018 Microsoft Corporation. All rights reserved. | |
| Released under Apache 2.0 license as described in the file LICENSE. | |
| Author: Leonardo de Moura | |
| -/ | |
| prelude | |
| import Init.Control.State | |
| import Init.Data.Int.Basic | |
| import Init.Data.String.Basic | |
| 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 | |
| | allOrNone | |
| | fill | |
| deriving Inhabited, BEq | |
| open Format in | |
| /-- A string with pretty-printing information for rendering in a column-width-aware way. | |
| 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 indent, | |
| or else add a newline if we are already at or past the indent. For example: | |
| ``` | |
| nest 2 <| "." ++ align ++ "a" ++ line ++ "b" | |
| ``` | |
| results in: | |
| ``` | |
| . a | |
| b | |
| ``` | |
| If `force` is true, then it will pad to the indent even if it is in a flattened group. | |
| -/ | |
| | align (force : Bool) : Format | |
| /-- A node containing a plain string. -/ | |
| | text : String → Format | |
| /-- `nest n f` tells the formatter that `f` is nested inside something with length `n` | |
| so that it is pretty-printed with the correct indentation on a line break. | |
| For example, we can define a formatter for list `l : List Format` as: | |
| ``` | |
| let f := join <| l.intersperse <| ", " ++ 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) : Format → Format | |
| /-- Concatenation of two Formats. -/ | |
| | 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 | |
| /-- Check 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 | |
| /-- Alias for a 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⟩ | |
| def join (xs : List Format) : Format := | |
| xs.foldl (·++·) "" | |
| 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 := s.posOf '\n' | |
| let off := s.offsetOfPos p | |
| { foundLine := p != s.endPos, foundFlattenedHardLine := flatten && p != s.endPos, 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 | |
| private structure WorkGroup where | |
| flatten : Bool | |
| 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.flatten (w + col - i.indent) w) | |
| (spaceUptoLine' ({ g with items := is }::gs) col) | |
| /-- A monad in which we can pretty-print `Format` objects. -/ | |
| class MonadPrettyFormat (m : Type → Type) where | |
| pushOutput (s : String) : m Unit | |
| pushNewline (indent : Nat) : m Unit | |
| currColumn : m Nat | |
| /-- Start a scope tagged with `n`. -/ | |
| startTag : Nat → m Unit | |
| /-- Exit the scope of `n`-many opened tags. -/ | |
| endTags : 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 := { flatten := 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 flatten := !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 := s.posOf '\n' | |
| if p == s.endPos then | |
| pushOutput s | |
| endTags i.activeTags | |
| be w (gs' is) | |
| else | |
| pushOutput (s.extract {} p) | |
| pushNewline i.indent.toNat | |
| let is := { i with f := text (s.extract (s.next p) s.endPos) }::is | |
| -- after a hard line break, re-evaluate whether to flatten the remaining group | |
| pushGroup g.flb is gs w >>= be w | |
| | line => | |
| match g.flb with | |
| | FlattenBehavior.allOrNone => | |
| if g.flatten 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.flatten then | |
| let gs'@(g'::_) ← pushGroup FlattenBehavior.fill is gs (w - " ".length) | |
| | panic "unreachable" | |
| if g'.flatten then | |
| pushOutput " " | |
| endTags i.activeTags | |
| be w gs' -- TODO: use `return` | |
| else | |
| breakHere | |
| else | |
| breakHere | |
| | align force => | |
| if g.flatten && !force then | |
| -- flatten (align false) = nil | |
| endTags i.activeTags | |
| be w (gs' is) | |
| else | |
| let k ← currColumn | |
| if k < i.indent then | |
| pushOutput ("".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.flatten 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. -/ | |
| def prettyM (f : Format) (w : Nat) (indent : Nat := 0) [Monad m] [MonadPrettyFormat m] : m Unit := | |
| be w [{ flb := FlattenBehavior.allOrNone, flatten := false, items := [{ f := f, indent, activeTags := 0 }]}] | |
| /-- Create a format `l ++ f ++ r` with a flatten group. | |
| FlattenBehaviour is `allOrNone`; for `fill` use `bracketFill`. -/ | |
| @[inline] def bracket (l : String) (f : Format) (r : String) : Format := | |
| group (nest l.length $ l ++ f ++ r) | |
| /-- Creates the format `"(" ++ f ++ ")"` with a flattening group.-/ | |
| @[inline] def paren (f : Format) : Format := | |
| bracket "(" f ")" | |
| /-- Creates the format `"[" ++ f ++ "]"` with a flattening group.-/ | |
| @[inline] def sbracket (f : Format) : Format := | |
| bracket "[" f "]" | |
| /-- Same as `bracket` except uses the `fill` flattening behaviour. -/ | |
| @[inline] def bracketFill (l : String) (f : Format) (r : String) : Format := | |
| fill (nest l.length $ l ++ f ++ r) | |
| /-- Default indentation. -/ | |
| def defIndent := 2 | |
| def defUnicode := true | |
| /-- Default width of the targeted output pane. -/ | |
| def defWidth := 120 | |
| /-- Nest with the default indentation 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 | |
| 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⟩ => ⟨out ++ s, col + s.length⟩ | |
| pushNewline indent := modify fun ⟨out, _⟩ => ⟨out ++ "\n".pushn ' ' 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`) | |
| -/ | |
| @[export lean_format_pretty] | |
| 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 | |
| /-- Class for converting a given type α to a `Format` object for pretty-printing. | |
| See also `Repr`, which also outputs a `Format` object. -/ | |
| class ToFormat (α : Type u) where | |
| 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 | |
| /-- Intersperse the given list (each item printed with `format`) with the given `sep` format. -/ | |
| def Format.joinSep {α : Type u} [ToFormat α] : List α → Format → Format | |
| | [], _ => nil | |
| | [a], _ => format a | |
| | a::as, sep => as.foldl (· ++ sep ++ format ·) (format a) | |
| /-- Format each item in `items` and prepend prefix `pre`. -/ | |
| def Format.prefixJoin {α : Type u} [ToFormat α] (pre : Format) : List α → Format | |
| | [] => nil | |
| | a::as => as.foldl (· ++ pre ++ format ·) (pre ++ format a) | |
| /-- Format each item in `items` and append `suffix`. -/ | |
| def Format.joinSuffix {α : Type u} [ToFormat α] : List α → Format → Format | |
| | [], _ => nil | |
| | a::as, suffix => as.foldl (· ++ format · ++ suffix) (format a ++ suffix) | |
| end Std |