Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework group and remove fail #126

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open

Conversation

1Jajen1
Copy link
Contributor

@1Jajen1 1Jajen1 commented Jan 22, 2020

Related to #120

I may have overdone it by removing Fail entirely because one compat module still needs it to convert from an external doc. Other than that this passes the tests (3 doctests fail with missing imports so I don't think they are related)

I'll comment more on this if needed, but for now this should show the idea ^^

Either way don't merge this, this probably needs to be tested a bit more.

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 22, 2020

@sjakobi let me know how this performs on your benchmarks, I am quite curious to see if this is an improvement :)

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 22, 2020

Impressive!

Unfortunately this broke a bunch of formatting tests in dhall:

Test suite tasty: RUNNING...
Dhall Tests
  format tests
    discover
      ./tests/format/unicode:                                                                                                  FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
          λ(isActive : Bool)
        →   { barLeftEnd = Some "┨"
            , barRightEnd = Some "┠"
            , separator = Some "┃"
            , alignment =
                < ToTheLeft | ToTheRight | Centered >.ToTheLeft : ./Alignment.dhall
            , barWidth = None Natural
            , barSegments = [ "index", "command", "path", "title" ]
            }
          : ./Bar.dhall
        
        
        Actual:
        
        λ(isActive : Bool) →   { barLeftEnd = Some "┨"
                               , barRightEnd = Some "┠"
                               , separator = Some "┃"
                               , alignment =   < ToTheLeft
                                               | ToTheRight
                                               | Centered
                                               >.ToTheLeft
                                             : ./Alignment.dhall
                               , barWidth = None Natural
                               , barSegments = [ "index", "command", "path", "title" ]
                               }
                             : ./Bar.dhall
        
        
        Expected (show): "  \955(isActive : Bool)\n\8594   { barLeftEnd = Some \"\9512\"\n    , barRightEnd = Some \"\9504\"\n    , separator = Some \"\9475\"\n    , alignment =\n        < ToTheLeft | ToTheRight | Centered >.ToTheLeft : ./Alignment.dhall\n    , barWidth = None Natural\n    , barSegments = [ \"index\", \"command\", \"path\", \"title\" ]\n    }\n  : ./Bar.dhall\n"
        Actual   (show): "\955(isActive : Bool) \8594   { barLeftEnd = Some \"\9512\"\n                       , barRightEnd = Some \"\9504\"\n                       , separator = Some \"\9475\"\n                       , alignment =   < ToTheLeft\n                                       | ToTheRight\n                                       | Centered\n                                       >.ToTheLeft\n                                     : ./Alignment.dhall\n                       , barWidth = None Natural\n                       , barSegments = [ \"index\", \"command\", \"path\", \"title\" ]\n                       }\n                     : ./Bar.dhall\n"
      ./tests/format/letLineCommentsAfterVariable:                                                                             FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let x
            -- xxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx
            : Natural
            = 0
        
        let y
            -- yyyyyyyyyy yyyyyyyyyyyyyyyyy yyyyyyyyyyyyyyyyy
            -- yyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyy
            : Natural
            = 1
        
        in  x + y
        
        
        Actual:
        
        let x
            -- xxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx
            : Natural
            = 0 let y
                    -- yyyyyyyyyy yyyyyyyyyyyyyyyyy yyyyyyyyyyyyyyyyy
                    -- yyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyy
                    : Natural
                    = 1 in x + y
        
        
        Expected (show): "let x\n    -- xxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx\n    : Natural\n    = 0\n\nlet y\n    -- yyyyyyyyyy yyyyyyyyyyyyyyyyy yyyyyyyyyyyyyyyyy\n    -- yyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyy\n    : Natural\n    = 1\n\nin  x + y\n"
        Actual   (show): "let x\n    -- xxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx\n    : Natural\n    = 0 let y\n            -- yyyyyyyyyy yyyyyyyyyyyyyyyyy yyyyyyyyyyyyyyyyy\n            -- yyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyy\n            : Natural\n            = 1 in x + y\n"
      ./tests/format/issue1301:                                                                                                FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let attribute
            : Text → Text → { mapKey : Text, mapValue : Text }
            = λ(key : Text) → λ(value : Text) → { mapKey = key, mapValue = value }
        
        in  attribute
        
        
        Actual:
        
        let attribute
            : Text → Text → { mapKey : Text, mapValue : Text }
            = λ(key : Text) → λ(value : Text) → { mapKey = key
                                                , mapValue = value
                                                } in attribute
        
        
        Expected (show): "let attribute\n    : Text \8594 Text \8594 { mapKey : Text, mapValue : Text }\n    = \955(key : Text) \8594 \955(value : Text) \8594 { mapKey = key, mapValue = value }\n\nin  attribute\n"
        Actual   (show): "let attribute\n    : Text \8594 Text \8594 { mapKey : Text, mapValue : Text }\n    = \955(key : Text) \8594 \955(value : Text) \8594 { mapKey = key\n                                        , mapValue = value\n                                        } in attribute\n"
      ./tests/format/issue183:                                                                                                 FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let foo = 1
        
        in    λ(bar : Integer)
            → let exposePort =
                      λ(portSpec : { ext : Integer, int : Integer })
                    → Integer/show portSpec.ext ++ ":" ++ Integer/show portSpec.int
        
              in  let exposeSamePort =
                        λ(port : Integer) → exposePort { ext = port, int = port }
        
                  in  { blah = bar }
        
        
        Actual:
        
        let foo = 1 in λ(bar : Integer) → let exposePort =   λ ( portSpec
                                                               : { ext : Integer
                                                                 , int : Integer
                                                                 }
                                                               )
                                                           →     Integer/show
                                                                   portSpec.ext
                                                             ++  ":"
                                                             ++  Integer/show
                                                                   portSpec.int
        
                                          in  let exposeSamePort =   λ(port : Integer)
                                                                   → exposePort
                                                                       { ext = port
                                                                       , int = port
                                                                       } in { blah = bar
                                                                            }
        
        
        Expected (show): "let foo = 1\n\nin    \955(bar : Integer)\n    \8594 let exposePort =\n              \955(portSpec : { ext : Integer, int : Integer })\n            \8594 Integer/show portSpec.ext ++ \":\" ++ Integer/show portSpec.int\n\n      in  let exposeSamePort =\n                \955(port : Integer) \8594 exposePort { ext = port, int = port }\n\n          in  { blah = bar }\n"
        Actual   (show): "let foo = 1 in \955(bar : Integer) \8594 let exposePort =   \955 ( portSpec\n                                                       : { ext : Integer\n                                                         , int : Integer\n                                                         }\n                                                       )\n                                                   \8594     Integer/show\n                                                           portSpec.ext\n                                                     ++  \":\"\n                                                     ++  Integer/show\n                                                           portSpec.int\n\n                                  in  let exposeSamePort =   \955(port : Integer)\n                                                           \8594 exposePort\n                                                               { ext = port\n                                                               , int = port\n                                                               } in { blah = bar\n                                                                    }\n"
      ./tests/format/concatSep:                                                                                                FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        {-
        Concatenate a `List` of `Text` values with a separator in between each value
        -}
        let Status = < Empty | NonEmpty : Text >
        
        let concatSep
            : ∀(separator : Text) → ∀(elements : List Text) → Text
            =   λ(separator : Text)
              → λ(elements : List Text)
              → let status =
                      List/fold
                        Text
                        elements
                        Status
                        (   λ(element : Text)
                          → λ(status : Status)
                          → merge
                              { Empty = Status.NonEmpty element
                              , NonEmpty =
                                    λ(result : Text)
                                  → Status.NonEmpty (element ++ separator ++ result)
                              }
                              status
                        )
                        Status.Empty
        
                in  merge { Empty = "", NonEmpty = λ(result : Text) → result } status
        
        let example0 = assert : concatSep ", " [ "ABC", "DEF", "GHI" ] ≡ "ABC, DEF, GHI"
        
        let example1 = assert : concatSep ", " ([] : List Text) ≡ ""
        
        in  concatSep
        
        
        Actual:
        
        {-
        Concatenate a `List` of `Text` values with a separator in between each value
        -}
        let Status = < Empty | NonEmpty : Text >
        
        let concatSep
            : ∀(separator : Text) → ∀(elements : List Text) → Text
            =   λ(separator : Text)
              → λ(elements : List Text)
              → let status = List/fold
                               Text
                               elements
                               Status
                               (   λ(element : Text)
                                 → λ(status : Status)
                                 → merge
                                     { Empty = Status.NonEmpty
                                                 element, NonEmpty =   λ(result : Text)
                                                                     → Status.NonEmpty
                                                                         (     element
                                                                           ++  separator
                                                                           ++  result
                                                                         ) }
                                     status
                               )
                               Status.Empty in merge
                                                 { Empty = "", NonEmpty =   λ ( result
                                                                              : Text
                                                                              )
                                                                          → result }
                                                 status
        
        let example0 = assert : concatSep ", " [ "ABC", "DEF", "GHI" ] ≡ "ABC, DEF, GHI"
        
        let example1 = assert : concatSep ", " ([] : List Text) ≡ ""
        
        in  concatSep
        
        
        Expected (show): "{-\nConcatenate a `List` of `Text` values with a separator in between each value\n-}\nlet Status = < Empty | NonEmpty : Text >\n\nlet concatSep\n    : \8704(separator : Text) \8594 \8704(elements : List Text) \8594 Text\n    =   \955(separator : Text)\n      \8594 \955(elements : List Text)\n      \8594 let status =\n              List/fold\n                Text\n                elements\n                Status\n                (   \955(element : Text)\n                  \8594 \955(status : Status)\n                  \8594 merge\n                      { Empty = Status.NonEmpty element\n                      , NonEmpty =\n                            \955(result : Text)\n                          \8594 Status.NonEmpty (element ++ separator ++ result)\n                      }\n                      status\n                )\n                Status.Empty\n\n        in  merge { Empty = \"\", NonEmpty = \955(result : Text) \8594 result } status\n\nlet example0 = assert : concatSep \", \" [ \"ABC\", \"DEF\", \"GHI\" ] \8801 \"ABC, DEF, GHI\"\n\nlet example1 = assert : concatSep \", \" ([] : List Text) \8801 \"\"\n\nin  concatSep\n"
        Actual   (show): "{-\nConcatenate a `List` of `Text` values with a separator in between each value\n-}\nlet Status = < Empty | NonEmpty : Text >\n\nlet concatSep\n    : \8704(separator : Text) \8594 \8704(elements : List Text) \8594 Text\n    =   \955(separator : Text)\n      \8594 \955(elements : List Text)\n      \8594 let status = List/fold\n                       Text\n                       elements\n                       Status\n                       (   \955(element : Text)\n                         \8594 \955(status : Status)\n                         \8594 merge\n                             { Empty = Status.NonEmpty\n                                         element, NonEmpty =   \955(result : Text)\n                                                             \8594 Status.NonEmpty\n                                                                 (     element\n                                                                   ++  separator\n                                                                   ++  result\n                                                                 ) }\n                             status\n                       )\n                       Status.Empty in merge\n                                         { Empty = \"\", NonEmpty =   \955 ( result\n                                                                      : Text\n                                                                      )\n                                                                  \8594 result }\n                                         status\n\nlet example0 = assert : concatSep \", \" [ \"ABC\", \"DEF\", \"GHI\" ] \8801 \"ABC, DEF, GHI\"\n\nlet example1 = assert : concatSep \", \" ([] : List Text) \8801 \"\"\n\nin  concatSep\n"
      ./tests/format/let:                                                                                                      FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let x = "Lorem ipsum"
        
        let y = "Lorem ipsum"
        
        in  let z = "Lorem ipsum" in x ++ y ++ z
        
        
        Actual:
        
        let x = "Lorem ipsum" let y = "Lorem ipsum" in let z = "Lorem ipsum" in     x
                                                                                ++  y
                                                                                ++  z
        
        
        Expected (show): "let x = \"Lorem ipsum\"\n\nlet y = \"Lorem ipsum\"\n\nin  let z = \"Lorem ipsum\" in x ++ y ++ z\n"
        Actual   (show): "let x = \"Lorem ipsum\" let y = \"Lorem ipsum\" in let z = \"Lorem ipsum\" in     x\n                                                                        ++  y\n                                                                        ++  z\n"
      ./tests/format/letShortComments:                                                                                         FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let x = True
        
        let {- 0 -} y
            {- 1 -} : {- 2 -} Bool
            = {- 3-} False
        
        in  x
        
        
        Actual:
        
        let x = True let {- 0 -} y
                         {- 1 -} : {- 2 -} Bool
                         = {- 3-} False in x
        
        
        Expected (show): "let x = True\n\nlet {- 0 -} y\n    {- 1 -} : {- 2 -} Bool\n    = {- 3-} False\n\nin  x\n"
        Actual   (show): "let x = True let {- 0 -} y\n                 {- 1 -} : {- 2 -} Bool\n                 = {- 3-} False in x\n"
      ./tests/format/multilineTrailingWhitespace:                                                                              FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        { x =
            ''
              
        
            foo  
            ''
        }
        
        
        Actual:
        
        { x = ''
                
        
              foo  
              '' }
        
        
        Expected (show): "{ x =\n    ''\n      \n\n    foo  \n    ''\n}\n"
        Actual   (show): "{ x = ''\n        \n\n      foo  \n      '' }\n"
      ./tests/format/ipfs:                                                                                                     FAIL (0.02s)
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let name = "ipfs"
        
        let labels =
              { `app.kubernetes.io/name` = name
              , `app.kubernetes.io/instance` = "wintering-rodent"
              , `app.kubernetes.io/version` = "0.4.0"
              , `app.kubernetes.io/managed-by` = "dhall"
              }
        
        let matchLabels =
              labels.{ `app.kubernetes.io/name`, `app.kubernetes.io/instance` }
        
        let k8s =
              https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/4ab28225a150498aef67c226d3c5f026c95b5a1e/package.dhall sha256:2c7ac35494f16b1f39afcf3467b2f3b0ab579edb0c711cddd2c93f1cbed358bd
        
        let serviceName = "ipfs"
        
        let apiPort = k8s.IntOrString.Int 5001
        
        let gatewayPort = k8s.IntOrString.Int 8080
        
        let toRule =
                λ ( args
                  : { host : Text
                    , path : Text
                    , serviceName : Text
                    , servicePort : k8s.IntOrString
                    }
                  )
              → k8s.IngressRule::{
                , host = Some args.host
                , http = Some k8s.HTTPIngressRuleValue::{
                  , paths =
                    [ k8s.HTTPIngressPath::{
                      , path = Some args.path
                      , backend = k8s.IngressBackend::args.{ serviceName, servicePort }
                      }
                    ]
                  }
                }
        
        in  [ k8s.Resource.Ingress
                k8s.Ingress::{
                , metadata = k8s.ObjectMeta::{
                  , labels = toMap labels
                  , name = "${name}-api"
                  }
                , spec = Some k8s.IngressSpec::{
                  , rules =
                    [ toRule
                        { host = "localhost"
                        , path = "/"
                        , serviceName = serviceName
                        , servicePort = gatewayPort
                        }
                    , toRule
                        { host = "localhost"
                        , path = "/"
                        , serviceName = serviceName
                        , servicePort = apiPort
                        }
                    ]
                  }
                }
            , k8s.Resource.Service
                k8s.Service::{
                , metadata = k8s.ObjectMeta::{
                  , name = serviceName
                  , labels = toMap labels
                  }
                , spec = Some k8s.ServiceSpec::{
                  , ports =
                    [ k8s.ServicePort::{
                      , port = 5001
                      , targetPort = Some apiPort
                      , name = Some "api"
                      }
                    , k8s.ServicePort::{
                      , port = 8080
                      , targetPort = Some gatewayPort
                      , name = Some "api"
                      }
                    ]
                  , selector = toMap matchLabels
                  }
                }
            , k8s.Resource.StatefulSet
                k8s.StatefulSet::{
                , metadata = k8s.ObjectMeta::{ name = name, labels = toMap labels }
                , spec = Some k8s.StatefulSetSpec::{
                  , serviceName = serviceName
                  , selector = k8s.LabelSelector::{ matchLabels = toMap matchLabels }
                  , template = k8s.PodTemplateSpec::{
                    , metadata = k8s.ObjectMeta::{ name = name, labels = toMap labels }
                    , spec = Some k8s.PodSpec::{
                      , securityContext = Some k8s.PodSecurityContext::{
                        , runAsUser = Some 1000
                        , runAsGroup = Some 1000
                        , fsGroup = Some 1000
                        }
                      , containers =
                        [ k8s.Container::{
                          , name = name
                          , image = Some "ipfs/go-ipfs:v0.4.22"
                          , livenessProbe = k8s.Probe::{
                            , httpGet = Some k8s.HTTPGetAction::{
                              , path = Some "/debug/metrics/prometheus"
                              , port = k8s.IntOrString.String "api"
                              }
                            , initialDelaySeconds = Some 15
                            , periodSeconds = Some 3
                            }
                          , readinessProbe = k8s.Probe::{
                            , httpGet = Some k8s.HTTPGetAction::{
                              , path = Some "/debug/metrics/prometheus"
                              , port = k8s.IntOrString.String "api"
                              }
                            , initialDelaySeconds = Some 15
                            , periodSeconds = Some 3
                            }
                          , ports =
                            [ k8s.ContainerPort::{
                              , containerPort = 5001
                              , name = Some "api"
                              }
                            , k8s.ContainerPort::{
                              , containerPort = 8080
                              , name = Some "gateway"
                              }
                            ]
                          , volumeMounts =
                            [ k8s.VolumeMount::{
                              , name = "ipfs-storage"
                              , mountPath = "/data/ipfs"
                              }
                            ]
                          }
                        ]
                      }
                    }
                  }
                }
            ]
        
        
        Actual:
        
        let name = "ipfs"
        
        let labels = { `app.kubernetes.io/name` = name
                     , `app.kubernetes.io/instance` = "wintering-rodent"
                     , `app.kubernetes.io/version` = "0.4.0"
                     , `app.kubernetes.io/managed-by` = "dhall"
                     }
        
        let matchLabels = labels.{ `app.kubernetes.io/name`
                                 , `app.kubernetes.io/instance`
                                 }
        
        let k8s =
              https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/4ab28225a150498aef67c226d3c5f026c95b5a1e/package.dhall sha256:2c7ac35494f16b1f39afcf3467b2f3b0ab579edb0c711cddd2c93f1cbed358bd
        
        let serviceName = "ipfs"
        
        let apiPort = k8s.IntOrString.Int 5001
        
        let gatewayPort = k8s.IntOrString.Int 8080
        
        let toRule =   λ ( args
                         : { host : Text
                           , path : Text
                           , serviceName : Text
                           , servicePort : k8s.IntOrString
                           }
                         )
                     → k8s.IngressRule::{
                       , host = Some args.host
                       , http = Some
                                  k8s.HTTPIngressRuleValue::{
                                  , paths =
                                    [ k8s.HTTPIngressPath::{
                                      , path = Some args.path
                                      , backend = k8s.IngressBackend::args.{ serviceName
                                                                           , servicePort
                                                                           }
                                      } ]
                                  }
                       }
        
        in  [ k8s.Resource.Ingress k8s.Ingress::{
                                   , metadata = k8s.ObjectMeta::{
                                                , labels = toMap labels
                                                , name = "${name}-api"
                                                }
                                   , spec = Some k8s.IngressSpec::{
                                                 , rules = [ toRule { host = "localhost"
                                                                    , path = "/"
                                                                    , serviceName =
                                                                        serviceName
                                                                    , servicePort =
                                                                        gatewayPort
                                                                    }
                                                           , toRule { host = "localhost"
                                                                    , path = "/"
                                                                    , serviceName =
                                                                        serviceName
                                                                    , servicePort =
                                                                        apiPort
                                                                    }
                                                           ]
                                                 }
                                   }
            , k8s.Resource.Service k8s.Service::{
                                   , metadata = k8s.ObjectMeta::{
                                                , name = serviceName
                                                , labels = toMap labels
                                                }
                                   , spec = Some k8s.ServiceSpec::{
                                                 , ports = [ k8s.ServicePort::{
                                                             , port = 5001
                                                             , targetPort = Some apiPort
                                                             , name = Some "api"
                                                             }, k8s.ServicePort::{
                                                                , port = 8080
                                                                , targetPort = Some
                                                                    gatewayPort
                                                                , name = Some "api"
                                                                } ]
                                                 , selector = toMap matchLabels
                                                 }
                                   }
            , k8s.Resource.StatefulSet
                k8s.StatefulSet::{
                , metadata = k8s.ObjectMeta::{ name = name, labels = toMap labels }
                , spec = Some k8s.StatefulSetSpec::{
                              , serviceName = serviceName
                              , selector = k8s.LabelSelector::{
                                           , matchLabels = toMap matchLabels
                                           }
                              , template = k8s.PodTemplateSpec::{
                                , metadata = k8s.ObjectMeta::{
                                             , name = name
                                             , labels = toMap labels
                                             }
                                , spec = Some k8s.PodSpec::{
                                  , securityContext = Some k8s.PodSecurityContext::{
                                                           , runAsUser = Some 1000
                                                           , runAsGroup = Some 1000
                                                           , fsGroup = Some 1000
                                                           }
                                  , containers =
                                    [ k8s.Container::{
                                      , name = name
                                      , image = Some "ipfs/go-ipfs:v0.4.22"
                                      , livenessProbe = k8s.Probe::{
                                        , httpGet = Some k8s.HTTPGetAction::{
                                                         , path = Some
                                                             "/debug/metrics/prometheus"
                                                         , port = k8s.IntOrString.String
                                                                    "api"
                                                         }
                                        , initialDelaySeconds = Some 15
                                        , periodSeconds = Some 3
                                        }
                                      , readinessProbe = k8s.Probe::{
                                        , httpGet = Some k8s.HTTPGetAction::{
                                                         , path = Some
                                                             "/debug/metrics/prometheus"
                                                         , port = k8s.IntOrString.String
                                                                    "api"
                                                         }
                                        , initialDelaySeconds = Some 15
                                        , periodSeconds = Some 3
                                        }
                                      , ports = [ k8s.ContainerPort::{
                                                  , containerPort = 5001
                                                  , name = Some "api"
                                                  }, k8s.ContainerPort::{
                                                     , containerPort = 8080
                                                     , name = Some "gateway"
                                                     } ]
                                      , volumeMounts = [ k8s.VolumeMount::{
                                                         , name = "ipfs-storage"
                                                         , mountPath = "/data/ipfs"
                                                         } ]
                                      } ]
                                  }
                                }
                              }
                }
            ]
        
        
        Expected (show): "let name = \"ipfs\"\n\nlet labels =\n      { `app.kubernetes.io/name` = name\n      , `app.kubernetes.io/instance` = \"wintering-rodent\"\n      , `app.kubernetes.io/version` = \"0.4.0\"\n      , `app.kubernetes.io/managed-by` = \"dhall\"\n      }\n\nlet matchLabels =\n      labels.{ `app.kubernetes.io/name`, `app.kubernetes.io/instance` }\n\nlet k8s =\n      https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/4ab28225a150498aef67c226d3c5f026c95b5a1e/package.dhall sha256:2c7ac35494f16b1f39afcf3467b2f3b0ab579edb0c711cddd2c93f1cbed358bd\n\nlet serviceName = \"ipfs\"\n\nlet apiPort = k8s.IntOrString.Int 5001\n\nlet gatewayPort = k8s.IntOrString.Int 8080\n\nlet toRule =\n        \955 ( args\n          : { host : Text\n            , path : Text\n            , serviceName : Text\n            , servicePort : k8s.IntOrString\n            }\n          )\n      \8594 k8s.IngressRule::{\n        , host = Some args.host\n        , http = Some k8s.HTTPIngressRuleValue::{\n          , paths =\n            [ k8s.HTTPIngressPath::{\n              , path = Some args.path\n              , backend = k8s.IngressBackend::args.{ serviceName, servicePort }\n              }\n            ]\n          }\n        }\n\nin  [ k8s.Resource.Ingress\n        k8s.Ingress::{\n        , metadata = k8s.ObjectMeta::{\n          , labels = toMap labels\n          , name = \"${name}-api\"\n          }\n        , spec = Some k8s.IngressSpec::{\n          , rules =\n            [ toRule\n                { host = \"localhost\"\n                , path = \"/\"\n                , serviceName = serviceName\n                , servicePort = gatewayPort\n                }\n            , toRule\n                { host = \"localhost\"\n                , path = \"/\"\n                , serviceName = serviceName\n                , servicePort = apiPort\n                }\n            ]\n          }\n        }\n    , k8s.Resource.Service\n        k8s.Service::{\n        , metadata = k8s.ObjectMeta::{\n          , name = serviceName\n          , labels = toMap labels\n          }\n        , spec = Some k8s.ServiceSpec::{\n          , ports =\n            [ k8s.ServicePort::{\n              , port = 5001\n              , targetPort = Some apiPort\n              , name = Some \"api\"\n              }\n            , k8s.ServicePort::{\n              , port = 8080\n              , targetPort = Some gatewayPort\n              , name = Some \"api\"\n              }\n            ]\n          , selector = toMap matchLabels\n          }\n        }\n    , k8s.Resource.StatefulSet\n        k8s.StatefulSet::{\n        , metadata = k8s.ObjectMeta::{ name = name, labels = toMap labels }\n        , spec = Some k8s.StatefulSetSpec::{\n          , serviceName = serviceName\n          , selector = k8s.LabelSelector::{ matchLabels = toMap matchLabels }\n          , template = k8s.PodTemplateSpec::{\n            , metadata = k8s.ObjectMeta::{ name = name, labels = toMap labels }\n            , spec = Some k8s.PodSpec::{\n              , securityContext = Some k8s.PodSecurityContext::{\n                , runAsUser = Some 1000\n                , runAsGroup = Some 1000\n                , fsGroup = Some 1000\n                }\n              , containers =\n                [ k8s.Container::{\n                  , name = name\n                  , image = Some \"ipfs/go-ipfs:v0.4.22\"\n                  , livenessProbe = k8s.Probe::{\n                    , httpGet = Some k8s.HTTPGetAction::{\n                      , path = Some \"/debug/metrics/prometheus\"\n                      , port = k8s.IntOrString.String \"api\"\n                      }\n                    , initialDelaySeconds = Some 15\n                    , periodSeconds = Some 3\n                    }\n                  , readinessProbe = k8s.Probe::{\n                    , httpGet = Some k8s.HTTPGetAction::{\n                      , path = Some \"/debug/metrics/prometheus\"\n                      , port = k8s.IntOrString.String \"api\"\n                      }\n                    , initialDelaySeconds = Some 15\n                    , periodSeconds = Some 3\n                    }\n                  , ports =\n                    [ k8s.ContainerPort::{\n                      , containerPort = 5001\n                      , name = Some \"api\"\n                      }\n                    , k8s.ContainerPort::{\n                      , containerPort = 8080\n                      , name = Some \"gateway\"\n                      }\n                    ]\n                  , volumeMounts =\n                    [ k8s.VolumeMount::{\n                      , name = \"ipfs-storage\"\n                      , mountPath = \"/data/ipfs\"\n                      }\n                    ]\n                  }\n                ]\n              }\n            }\n          }\n        }\n    ]\n"
        Actual   (show): "let name = \"ipfs\"\n\nlet labels = { `app.kubernetes.io/name` = name\n             , `app.kubernetes.io/instance` = \"wintering-rodent\"\n             , `app.kubernetes.io/version` = \"0.4.0\"\n             , `app.kubernetes.io/managed-by` = \"dhall\"\n             }\n\nlet matchLabels = labels.{ `app.kubernetes.io/name`\n                         , `app.kubernetes.io/instance`\n                         }\n\nlet k8s =\n      https://raw.githubusercontent.com/dhall-lang/dhall-kubernetes/4ab28225a150498aef67c226d3c5f026c95b5a1e/package.dhall sha256:2c7ac35494f16b1f39afcf3467b2f3b0ab579edb0c711cddd2c93f1cbed358bd\n\nlet serviceName = \"ipfs\"\n\nlet apiPort = k8s.IntOrString.Int 5001\n\nlet gatewayPort = k8s.IntOrString.Int 8080\n\nlet toRule =   \955 ( args\n                 : { host : Text\n                   , path : Text\n                   , serviceName : Text\n                   , servicePort : k8s.IntOrString\n                   }\n                 )\n             \8594 k8s.IngressRule::{\n               , host = Some args.host\n               , http = Some\n                          k8s.HTTPIngressRuleValue::{\n                          , paths =\n                            [ k8s.HTTPIngressPath::{\n                              , path = Some args.path\n                              , backend = k8s.IngressBackend::args.{ serviceName\n                                                                   , servicePort\n                                                                   }\n                              } ]\n                          }\n               }\n\nin  [ k8s.Resource.Ingress k8s.Ingress::{\n                           , metadata = k8s.ObjectMeta::{\n                                        , labels = toMap labels\n                                        , name = \"${name}-api\"\n                                        }\n                           , spec = Some k8s.IngressSpec::{\n                                         , rules = [ toRule { host = \"localhost\"\n                                                            , path = \"/\"\n                                                            , serviceName =\n                                                                serviceName\n                                                            , servicePort =\n                                                                gatewayPort\n                                                            }\n                                                   , toRule { host = \"localhost\"\n                                                            , path = \"/\"\n                                                            , serviceName =\n                                                                serviceName\n                                                            , servicePort =\n                                                                apiPort\n                                                            }\n                                                   ]\n                                         }\n                           }\n    , k8s.Resource.Service k8s.Service::{\n                           , metadata = k8s.ObjectMeta::{\n                                        , name = serviceName\n                                        , labels = toMap labels\n                                        }\n                           , spec = Some k8s.ServiceSpec::{\n                                         , ports = [ k8s.ServicePort::{\n                                                     , port = 5001\n                                                     , targetPort = Some apiPort\n                                                     , name = Some \"api\"\n                                                     }, k8s.ServicePort::{\n                                                        , port = 8080\n                                                        , targetPort = Some\n                                                            gatewayPort\n                                                        , name = Some \"api\"\n                                                        } ]\n                                         , selector = toMap matchLabels\n                                         }\n                           }\n    , k8s.Resource.StatefulSet\n        k8s.StatefulSet::{\n        , metadata = k8s.ObjectMeta::{ name = name, labels = toMap labels }\n        , spec = Some k8s.StatefulSetSpec::{\n                      , serviceName = serviceName\n                      , selector = k8s.LabelSelector::{\n                                   , matchLabels = toMap matchLabels\n                                   }\n                      , template = k8s.PodTemplateSpec::{\n                        , metadata = k8s.ObjectMeta::{\n                                     , name = name\n                                     , labels = toMap labels\n                                     }\n                        , spec = Some k8s.PodSpec::{\n                          , securityContext = Some k8s.PodSecurityContext::{\n                                                   , runAsUser = Some 1000\n                                                   , runAsGroup = Some 1000\n                                                   , fsGroup = Some 1000\n                                                   }\n                          , containers =\n                            [ k8s.Container::{\n                              , name = name\n                              , image = Some \"ipfs/go-ipfs:v0.4.22\"\n                              , livenessProbe = k8s.Probe::{\n                                , httpGet = Some k8s.HTTPGetAction::{\n                                                 , path = Some\n                                                     \"/debug/metrics/prometheus\"\n                                                 , port = k8s.IntOrString.String\n                                                            \"api\"\n                                                 }\n                                , initialDelaySeconds = Some 15\n                                , periodSeconds = Some 3\n                                }\n                              , readinessProbe = k8s.Probe::{\n                                , httpGet = Some k8s.HTTPGetAction::{\n                                                 , path = Some\n                                                     \"/debug/metrics/prometheus\"\n                                                 , port = k8s.IntOrString.String\n                                                            \"api\"\n                                                 }\n                                , initialDelaySeconds = Some 15\n                                , periodSeconds = Some 3\n                                }\n                              , ports = [ k8s.ContainerPort::{\n                                          , containerPort = 5001\n                                          , name = Some \"api\"\n                                          }, k8s.ContainerPort::{\n                                             , containerPort = 8080\n                                             , name = Some \"gateway\"\n                                             } ]\n                              , volumeMounts = [ k8s.VolumeMount::{\n                                                 , name = \"ipfs-storage\"\n                                                 , mountPath = \"/data/ipfs\"\n                                                 } ]\n                              } ]\n                          }\n                        }\n                      }\n        }\n    ]\n"
      ./tests/format/largeRecord:                                                                                              FAIL (0.01s)
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        -- This file was tested using console NetHack version 3.6.1
        --
        -- To exercise all options documented in https://nethackwiki.com/wiki/Options
        -- see `./unvalidated.dhall`
        
        let types = ./../types.dhall
        
        let defaults = ./../defaults.dhall
        
        in    defaults.Config
            ⫽ { AUTOCOMPLETE =
                [ { enable = True, value = "zap" }
                , { enable = False, value = "annotate" }
                ]
              , acoustics = Some True
              , align = Some { enable = True, value = types.Alignment.chaotic }
              , autodescribe = Some False
              , autodig = Some False
              , AUTOPICKUP_EXCEPTION =
                [ { pickup = False, name = "chest" }
                , { pickup = True, name = "dagger" }
                ]
              , BIND =
                [ { keybinding = "!", command = "loot" }
                , { keybinding = "^v", command = "untrap" }
                , { keybinding = "M-x", command = "terrain" }
                ]
              , catname = Some "Mirri"
              , checkpoint = Some True
              , checkspace = Some True
              , clicklook = Some False
              , cmdassist = Some True
              , confirm = Some True
              , dark_room = Some False
              , disclose = Some
                  (   defaults.Disclose
                    ⫽ { inventory = Some { prompt = True, default = True }
                      , attributes = Some { prompt = True, default = False }
                      , monsters_killed = Some { prompt = False, default = True }
                      , monsters_genocided = Some { prompt = False, default = False }
                      , conduct = Some { prompt = False, default = False }
                      , dungeon_overview = Some { prompt = False, default = False }
                      }
                  )
              , dogname = Some "Cujo"
              , extmenu = Some False
              , fixinv = Some True
              , force_invmenu = Some False
              , fruit = Some "slime mold"
              , gender = Some types.Gender.female
              , goldX = Some False
              , help = Some True
              , hilite_pet = Some False
              , hilite_pile = Some False
              , hilite_status =
                    defaults.HiliteStatus
                  ⫽ { gold =
                      [ { color = types.Color.yellow
                        , trigger = Some types.Numeric.always
                        , attributes = None types.Attributes
                        }
                      ]
                    }
              , hitpointbar = Some True
              , horsename = Some "Erhir"
              , ignintr = Some False
              , implicit_uncursed = Some True
              , legacy = Some True
              , lit_corridor = Some False
              , lootabc = Some False
              , mail = Some True
              , mention_walls = Some False
              , menucolors =
                [ { regex = "blessed"
                  , color = Some types.Color.cyan
                  , attributes = defaults.Attributes ⫽ { bold = Some True }
                  }
                ]
              , menustyle = Some types.MenuStyle.traditional
              , menu_deselect_all = Some "-"
              , menu_deselect_page = Some "\\"
              , menu_first_page = Some "^"
              , menu_headings = Some types.MenuHeadings.bold
              , menu_invert_all = Some "@"
              , menu_invert_page = Some "~"
              , menu_last_page = Some "|"
              , menu_next_page = Some ">"
              , menu_objsyms = Some False
              , menu_previous_page = Some "<"
              , menu_search = Some ":"
              , menu_select_all = Some "."
              , menu_tab_sep = Some False
              , msg_window = Some types.MsgWindow.single
              , MSGTYPE = [ types.MsgType.hide "You swap places with .*" ]
              , name = Some "Kaeru"
              , news = Some True
              , nudist = Some False
              , null = Some False
              , number_pad = Some types.NumberPad.Letters
              , packorder = Some "\")[%?+!=/(*`0_"
              , paranoid_confirmation =
                  defaults.ParanoidConfirmation ⫽ { pray = Some True }
              , pettype = Some types.PetType.cat
              , pickup_burden = Some types.PickupBurden.stressed
              , pickup_thrown = Some True
              , pickup_types = Some "?!/"
              , pile_limit = Some (types.PileLimit.limit 5)
              , playmode = Some types.PlayMode.normal
              , pushweapon = Some False
              , race = Some { enable = True, value = types.Race.elf }
              , rest_on_space = Some False
              , role = Some { enable = True, value = types.Role.wizard }
              , roguesymset = Some types.SymSet.RogueEpyx
              , runmode = Some types.RunMode.walk
              , safe_pet = Some True
              , sanity_check = Some False
              , scores = { own = Some True, around = Some 2, top = Some 10 }
              , showexp = Some False
              , showrace = Some False
              , showscore = Some False
              , silent = Some True
              , sortloot = Some types.SortLoot.none
              , sortpack = Some True
              , sparkle = Some True
              , standout = Some False
              , status_updates = Some True
              , statushilites = Some 10
              , suppress_alert = Some "3.3.1"
              , symset = Some types.SymSet.DECgraphics
              , time = Some False
              , timed_delay = Some True
              , tombstone = Some True
              , toptenwin = Some False
              , travel = Some True
              , verbose = Some True
              , whatis_coord = Some types.WhatisCoord.none
              , whatis_filter = Some types.WhatisFilter.no_filtering
              , whatis_menu = Some False
              , whatis_moveskip = Some False
              , windowtype = Some "tty"
              , wizkit = Some "wizkit.txt"
              }
        
        
        Actual:
        
        -- This file was tested using console NetHack version 3.6.1
        --
        -- To exercise all options documented in https://nethackwiki.com/wiki/Options
        -- see `./unvalidated.dhall`
        
        let types = ./../types.dhall
        
        let defaults = ./../defaults.dhall
        
        in  defaults.Config ⫽ { AUTOCOMPLETE = [ { enable = True, value = "zap" }
                                               , { enable = False, value = "annotate" }
                                               ]
                              , acoustics = Some True
                              , align = Some { enable = True
                                             , value = types.Alignment.chaotic
                                             }
                              , autodescribe = Some False
                              , autodig = Some False
                              , AUTOPICKUP_EXCEPTION = [ { pickup = False
                                                         , name = "chest"
                                                         }
                                                       , { pickup = True
                                                         , name = "dagger"
                                                         }
                                                       ]
                              , BIND = [ { keybinding = "!", command = "loot" }
                                       , { keybinding = "^v", command = "untrap" }
                                       , { keybinding = "M-x", command = "terrain" }
                                       ]
                              , catname = Some "Mirri"
                              , checkpoint = Some True
                              , checkspace = Some True
                              , clicklook = Some False
                              , cmdassist = Some True
                              , confirm = Some True
                              , dark_room = Some False
                              , disclose = Some (   defaults.Disclose
                                                  ⫽ { inventory = Some { prompt = True
                                                                       , default = True
                                                                       }
                                                    , attributes = Some { prompt = True
                                                                        , default =
                                                                            False
                                                                        }
                                                    , monsters_killed = Some { prompt =
                                                                                 False
                                                                             , default =
                                                                                 True
                                                                             }
                                                    , monsters_genocided = Some
                                                                             { prompt =
                                                                                 False
                                                                             , default =
                                                                                 False
                                                                             }
                                                    , conduct = Some { prompt = False
                                                                     , default = False
                                                                     }
                                                    , dungeon_overview = Some
                                                                           { prompt =
                                                                               False
                                                                           , default =
                                                                               False
                                                                           }
                                                    }
                                                )
                              , dogname = Some "Cujo"
                              , extmenu = Some False
                              , fixinv = Some True
                              , force_invmenu = Some False
                              , fruit = Some "slime mold"
                              , gender = Some types.Gender.female
                              , goldX = Some False
                              , help = Some True
                              , hilite_pet = Some False
                              , hilite_pile = Some False
                              , hilite_status =   defaults.HiliteStatus
                                                ⫽ { gold = [ { color =
                                                                 types.Color.yellow
                                                             , trigger = Some
                                                                 types.Numeric.always
                                                             , attributes =
                                                                 None types.Attributes
                                                             }
                                                           ] }
                              , hitpointbar = Some True
                              , horsename = Some "Erhir"
                              , ignintr = Some False
                              , implicit_uncursed = Some True
                              , legacy = Some True
                              , lit_corridor = Some False
                              , lootabc = Some False
                              , mail = Some True
                              , mention_walls = Some False
                              , menucolors = [ { regex = "blessed"
                                               , color = Some types.Color.cyan
                                               , attributes =   defaults.Attributes
                                                              ⫽ { bold = Some True }
                                               }
                                             ]
                              , menustyle = Some types.MenuStyle.traditional
                              , menu_deselect_all = Some "-"
                              , menu_deselect_page = Some "\\"
                              , menu_first_page = Some "^"
                              , menu_headings = Some types.MenuHeadings.bold
                              , menu_invert_all = Some "@"
                              , menu_invert_page = Some "~"
                              , menu_last_page = Some "|"
                              , menu_next_page = Some ">"
                              , menu_objsyms = Some False
                              , menu_previous_page = Some "<"
                              , menu_search = Some ":"
                              , menu_select_all = Some "."
                              , menu_tab_sep = Some False
                              , msg_window = Some types.MsgWindow.single
                              , MSGTYPE = [ types.MsgType.hide "You swap places with .*"
                                          ]
                              , name = Some "Kaeru"
                              , news = Some True
                              , nudist = Some False
                              , null = Some False
                              , number_pad = Some types.NumberPad.Letters
                              , packorder = Some "\")[%?+!=/(*`0_"
                              , paranoid_confirmation =   defaults.ParanoidConfirmation
                                                        ⫽ { pray = Some True }
                              , pettype = Some types.PetType.cat
                              , pickup_burden = Some types.PickupBurden.stressed
                              , pickup_thrown = Some True
                              , pickup_types = Some "?!/"
                              , pile_limit = Some (types.PileLimit.limit 5)
                              , playmode = Some types.PlayMode.normal
                              , pushweapon = Some False
                              , race = Some { enable = True, value = types.Race.elf }
                              , rest_on_space = Some False
                              , role = Some { enable = True, value = types.Role.wizard }
                              , roguesymset = Some types.SymSet.RogueEpyx
                              , runmode = Some types.RunMode.walk
                              , safe_pet = Some True
                              , sanity_check = Some False
                              , scores = { own = Some True, around = Some
                                                                       2, top = Some
                                                                                  10 }
                              , showexp = Some False
                              , showrace = Some False
                              , showscore = Some False
                              , silent = Some True
                              , sortloot = Some types.SortLoot.none
                              , sortpack = Some True
                              , sparkle = Some True
                              , standout = Some False
                              , status_updates = Some True
                              , statushilites = Some 10
                              , suppress_alert = Some "3.3.1"
                              , symset = Some types.SymSet.DECgraphics
                              , time = Some False
                              , timed_delay = Some True
                              , tombstone = Some True
                              , toptenwin = Some False
                              , travel = Some True
                              , verbose = Some True
                              , whatis_coord = Some types.WhatisCoord.none
                              , whatis_filter = Some types.WhatisFilter.no_filtering
                              , whatis_menu = Some False
                              , whatis_moveskip = Some False
                              , windowtype = Some "tty"
                              , wizkit = Some "wizkit.txt"
                              }
        
        
        Expected (show): "-- This file was tested using console NetHack version 3.6.1\n--\n-- To exercise all options documented in https://nethackwiki.com/wiki/Options\n-- see `./unvalidated.dhall`\n\nlet types = ./../types.dhall\n\nlet defaults = ./../defaults.dhall\n\nin    defaults.Config\n    \11005 { AUTOCOMPLETE =\n        [ { enable = True, value = \"zap\" }\n        , { enable = False, value = \"annotate\" }\n        ]\n      , acoustics = Some True\n      , align = Some { enable = True, value = types.Alignment.chaotic }\n      , autodescribe = Some False\n      , autodig = Some False\n      , AUTOPICKUP_EXCEPTION =\n        [ { pickup = False, name = \"chest\" }\n        , { pickup = True, name = \"dagger\" }\n        ]\n      , BIND =\n        [ { keybinding = \"!\", command = \"loot\" }\n        , { keybinding = \"^v\", command = \"untrap\" }\n        , { keybinding = \"M-x\", command = \"terrain\" }\n        ]\n      , catname = Some \"Mirri\"\n      , checkpoint = Some True\n      , checkspace = Some True\n      , clicklook = Some False\n      , cmdassist = Some True\n      , confirm = Some True\n      , dark_room = Some False\n      , disclose = Some\n          (   defaults.Disclose\n            \11005 { inventory = Some { prompt = True, default = True }\n              , attributes = Some { prompt = True, default = False }\n              , monsters_killed = Some { prompt = False, default = True }\n              , monsters_genocided = Some { prompt = False, default = False }\n              , conduct = Some { prompt = False, default = False }\n              , dungeon_overview = Some { prompt = False, default = False }\n              }\n          )\n      , dogname = Some \"Cujo\"\n      , extmenu = Some False\n      , fixinv = Some True\n      , force_invmenu = Some False\n      , fruit = Some \"slime mold\"\n      , gender = Some types.Gender.female\n      , goldX = Some False\n      , help = Some True\n      , hilite_pet = Some False\n      , hilite_pile = Some False\n      , hilite_status =\n            defaults.HiliteStatus\n          \11005 { gold =\n              [ { color = types.Color.yellow\n                , trigger = Some types.Numeric.always\n                , attributes = None types.Attributes\n                }\n              ]\n            }\n      , hitpointbar = Some True\n      , horsename = Some \"Erhir\"\n      , ignintr = Some False\n      , implicit_uncursed = Some True\n      , legacy = Some True\n      , lit_corridor = Some False\n      , lootabc = Some False\n      , mail = Some True\n      , mention_walls = Some False\n      , menucolors =\n        [ { regex = \"blessed\"\n          , color = Some types.Color.cyan\n          , attributes = defaults.Attributes \11005 { bold = Some True }\n          }\n        ]\n      , menustyle = Some types.MenuStyle.traditional\n      , menu_deselect_all = Some \"-\"\n      , menu_deselect_page = Some \"\\\\\"\n      , menu_first_page = Some \"^\"\n      , menu_headings = Some types.MenuHeadings.bold\n      , menu_invert_all = Some \"@\"\n      , menu_invert_page = Some \"~\"\n      , menu_last_page = Some \"|\"\n      , menu_next_page = Some \">\"\n      , menu_objsyms = Some False\n      , menu_previous_page = Some \"<\"\n      , menu_search = Some \":\"\n      , menu_select_all = Some \".\"\n      , menu_tab_sep = Some False\n      , msg_window = Some types.MsgWindow.single\n      , MSGTYPE = [ types.MsgType.hide \"You swap places with .*\" ]\n      , name = Some \"Kaeru\"\n      , news = Some True\n      , nudist = Some False\n      , null = Some False\n      , number_pad = Some types.NumberPad.Letters\n      , packorder = Some \"\\\")[%?+!=/(*`0_\"\n      , paranoid_confirmation =\n          defaults.ParanoidConfirmation \11005 { pray = Some True }\n      , pettype = Some types.PetType.cat\n      , pickup_burden = Some types.PickupBurden.stressed\n      , pickup_thrown = Some True\n      , pickup_types = Some \"?!/\"\n      , pile_limit = Some (types.PileLimit.limit 5)\n      , playmode = Some types.PlayMode.normal\n      , pushweapon = Some False\n      , race = Some { enable = True, value = types.Race.elf }\n      , rest_on_space = Some False\n      , role = Some { enable = True, value = types.Role.wizard }\n      , roguesymset = Some types.SymSet.RogueEpyx\n      , runmode = Some types.RunMode.walk\n      , safe_pet = Some True\n      , sanity_check = Some False\n      , scores = { own = Some True, around = Some 2, top = Some 10 }\n      , showexp = Some False\n      , showrace = Some False\n      , showscore = Some False\n      , silent = Some True\n      , sortloot = Some types.SortLoot.none\n      , sortpack = Some True\n      , sparkle = Some True\n      , standout = Some False\n      , status_updates = Some True\n      , statushilites = Some 10\n      , suppress_alert = Some \"3.3.1\"\n      , symset = Some types.SymSet.DECgraphics\n      , time = Some False\n      , timed_delay = Some True\n      , tombstone = Some True\n      , toptenwin = Some False\n      , travel = Some True\n      , verbose = Some True\n      , whatis_coord = Some types.WhatisCoord.none\n      , whatis_filter = Some types.WhatisFilter.no_filtering\n      , whatis_menu = Some False\n      , whatis_moveskip = Some False\n      , windowtype = Some \"tty\"\n      , wizkit = Some \"wizkit.txt\"\n      }\n"
        Actual   (show): "-- This file was tested using console NetHack version 3.6.1\n--\n-- To exercise all options documented in https://nethackwiki.com/wiki/Options\n-- see `./unvalidated.dhall`\n\nlet types = ./../types.dhall\n\nlet defaults = ./../defaults.dhall\n\nin  defaults.Config \11005 { AUTOCOMPLETE = [ { enable = True, value = \"zap\" }\n                                       , { enable = False, value = \"annotate\" }\n                                       ]\n                      , acoustics = Some True\n                      , align = Some { enable = True\n                                     , value = types.Alignment.chaotic\n                                     }\n                      , autodescribe = Some False\n                      , autodig = Some False\n                      , AUTOPICKUP_EXCEPTION = [ { pickup = False\n                                                 , name = \"chest\"\n                                                 }\n                                               , { pickup = True\n                                                 , name = \"dagger\"\n                                                 }\n                                               ]\n                      , BIND = [ { keybinding = \"!\", command = \"loot\" }\n                               , { keybinding = \"^v\", command = \"untrap\" }\n                               , { keybinding = \"M-x\", command = \"terrain\" }\n                               ]\n                      , catname = Some \"Mirri\"\n                      , checkpoint = Some True\n                      , checkspace = Some True\n                      , clicklook = Some False\n                      , cmdassist = Some True\n                      , confirm = Some True\n                      , dark_room = Some False\n                      , disclose = Some (   defaults.Disclose\n                                          \11005 { inventory = Some { prompt = True\n                                                               , default = True\n                                                               }\n                                            , attributes = Some { prompt = True\n                                                                , default =\n                                                                    False\n                                                                }\n                                            , monsters_killed = Some { prompt =\n                                                                         False\n                                                                     , default =\n                                                                         True\n                                                                     }\n                                            , monsters_genocided = Some\n                                                                     { prompt =\n                                                                         False\n                                                                     , default =\n                                                                         False\n                                                                     }\n                                            , conduct = Some { prompt = False\n                                                             , default = False\n                                                             }\n                                            , dungeon_overview = Some\n                                                                   { prompt =\n                                                                       False\n                                                                   , default =\n                                                                       False\n                                                                   }\n                                            }\n                                        )\n                      , dogname = Some \"Cujo\"\n                      , extmenu = Some False\n                      , fixinv = Some True\n                      , force_invmenu = Some False\n                      , fruit = Some \"slime mold\"\n                      , gender = Some types.Gender.female\n                      , goldX = Some False\n                      , help = Some True\n                      , hilite_pet = Some False\n                      , hilite_pile = Some False\n                      , hilite_status =   defaults.HiliteStatus\n                                        \11005 { gold = [ { color =\n                                                         types.Color.yellow\n                                                     , trigger = Some\n                                                         types.Numeric.always\n                                                     , attributes =\n                                                         None types.Attributes\n                                                     }\n                                                   ] }\n                      , hitpointbar = Some True\n                      , horsename = Some \"Erhir\"\n                      , ignintr = Some False\n                      , implicit_uncursed = Some True\n                      , legacy = Some True\n                      , lit_corridor = Some False\n                      , lootabc = Some False\n                      , mail = Some True\n                      , mention_walls = Some False\n                      , menucolors = [ { regex = \"blessed\"\n                                       , color = Some types.Color.cyan\n                                       , attributes =   defaults.Attributes\n                                                      \11005 { bold = Some True }\n                                       }\n                                     ]\n                      , menustyle = Some types.MenuStyle.traditional\n                      , menu_deselect_all = Some \"-\"\n                      , menu_deselect_page = Some \"\\\\\"\n                      , menu_first_page = Some \"^\"\n                      , menu_headings = Some types.MenuHeadings.bold\n                      , menu_invert_all = Some \"@\"\n                      , menu_invert_page = Some \"~\"\n                      , menu_last_page = Some \"|\"\n                      , menu_next_page = Some \">\"\n                      , menu_objsyms = Some False\n                      , menu_previous_page = Some \"<\"\n                      , menu_search = Some \":\"\n                      , menu_select_all = Some \".\"\n                      , menu_tab_sep = Some False\n                      , msg_window = Some types.MsgWindow.single\n                      , MSGTYPE = [ types.MsgType.hide \"You swap places with .*\"\n                                  ]\n                      , name = Some \"Kaeru\"\n                      , news = Some True\n                      , nudist = Some False\n                      , null = Some False\n                      , number_pad = Some types.NumberPad.Letters\n                      , packorder = Some \"\\\")[%?+!=/(*`0_\"\n                      , paranoid_confirmation =   defaults.ParanoidConfirmation\n                                                \11005 { pray = Some True }\n                      , pettype = Some types.PetType.cat\n                      , pickup_burden = Some types.PickupBurden.stressed\n                      , pickup_thrown = Some True\n                      , pickup_types = Some \"?!/\"\n                      , pile_limit = Some (types.PileLimit.limit 5)\n                      , playmode = Some types.PlayMode.normal\n                      , pushweapon = Some False\n                      , race = Some { enable = True, value = types.Race.elf }\n                      , rest_on_space = Some False\n                      , role = Some { enable = True, value = types.Role.wizard }\n                      , roguesymset = Some types.SymSet.RogueEpyx\n                      , runmode = Some types.RunMode.walk\n                      , safe_pet = Some True\n                      , sanity_check = Some False\n                      , scores = { own = Some True, around = Some\n                                                               2, top = Some\n                                                                          10 }\n                      , showexp = Some False\n                      , showrace = Some False\n                      , showscore = Some False\n                      , silent = Some True\n                      , sortloot = Some types.SortLoot.none\n                      , sortpack = Some True\n                      , sparkle = Some True\n                      , standout = Some False\n                      , status_updates = Some True\n                      , statushilites = Some 10\n                      , suppress_alert = Some \"3.3.1\"\n                      , symset = Some types.SymSet.DECgraphics\n                      , time = Some False\n                      , timed_delay = Some True\n                      , tombstone = Some True\n                      , toptenwin = Some False\n                      , travel = Some True\n                      , verbose = Some True\n                      , whatis_coord = Some types.WhatisCoord.none\n                      , whatis_filter = Some types.WhatisFilter.no_filtering\n                      , whatis_menu = Some False\n                      , whatis_moveskip = Some False\n                      , windowtype = Some \"tty\"\n                      , wizkit = Some \"wizkit.txt\"\n                      }\n"
      ./tests/format/letNewlineComments:                                                                                       FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let x = 1
        
        in  let {- aaaaaaaaaaaaaaaaaaa
                -} y
                : {- bbbbbbbbbbbbbbbbbbbbb
                  -} Natural
                = {- ddddddddddddddddd -} 2
        
            in  x
        
        
        Actual:
        
        let x = 1 in let {- aaaaaaaaaaaaaaaaaaa
                         -} y
                         : {- bbbbbbbbbbbbbbbbbbbbb
                           -} Natural
                         = {- ddddddddddddddddd -} 2 in x
        
        
        Expected (show): "let x = 1\n\nin  let {- aaaaaaaaaaaaaaaaaaa\n        -} y\n        : {- bbbbbbbbbbbbbbbbbbbbb\n          -} Natural\n        = {- ddddddddddddddddd -} 2\n\n    in  x\n"
        Actual   (show): "let x = 1 in let {- aaaaaaaaaaaaaaaaaaa\n                 -} y\n                 : {- bbbbbbbbbbbbbbbbbbbbb\n                   -} Natural\n                 = {- ddddddddddddddddd -} 2 in x\n"
      ./tests/format/issue1413:                                                                                                FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let foo =
        
            {- test -}
              "hello"
        
        in  foo
        
        
        Actual:
        
        let foo =
        
                      {- test -}
                  "hello" in foo
        
        
        Expected (show): "let foo =\n\n    {- test -}\n      \"hello\"\n\nin  foo\n"
        Actual   (show): "let foo =\n\n              {- test -}\n          \"hello\" in foo\n"
      ./tests/format/letComments:                                                                                              FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        let {- Example documentation for a let binding
               that spans more than one line
            -}
            x {- we can interject things here -} =
            -- Make sure one-line comments work
              1
        
        in  x
        
        
        Actual:
        
        let {- Example documentation for a let binding
               that spans more than one line
            -}
            x {- we can interject things here -} = -- Make sure one-line comments work
                                                   1 in x
        
        
        Expected (show): "let {- Example documentation for a let binding\n       that spans more than one line\n    -}\n    x {- we can interject things here -} =\n    -- Make sure one-line comments work\n      1\n\nin  x\n"
        Actual   (show): "let {- Example documentation for a let binding\n       that spans more than one line\n    -}\n    x {- we can interject things here -} = -- Make sure one-line comments work\n                                           1 in x\n"
      ./tests/format/recordCompletion:                                                                                         FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        { field0 = T::{
          , a = 1
          , b = 1
          , c = 1
          , d = 1
          , e = 1
          , f = 1
          , g = 1
          , h = 1
          , i = 1
          , j = 1
          , k = 1
          , l = 1
          , m = 1
          , n = 1
          , o = 1
          , p = 1
          , q = 1
          , r = 1
          , s = 1
          , t = 1
          }
        , field1 = T::{ a = 1 }
        }
        
        
        Actual:
        
        { field0 = T::{
                   , a = 1
                   , b = 1
                   , c = 1
                   , d = 1
                   , e = 1
                   , f = 1
                   , g = 1
                   , h = 1
                   , i = 1
                   , j = 1
                   , k = 1
                   , l = 1
                   , m = 1
                   , n = 1
                   , o = 1
                   , p = 1
                   , q = 1
                   , r = 1
                   , s = 1
                   , t = 1
                   }, field1 = T::{ a = 1 } }
        
        
        Expected (show): "{ field0 = T::{\n  , a = 1\n  , b = 1\n  , c = 1\n  , d = 1\n  , e = 1\n  , f = 1\n  , g = 1\n  , h = 1\n  , i = 1\n  , j = 1\n  , k = 1\n  , l = 1\n  , m = 1\n  , n = 1\n  , o = 1\n  , p = 1\n  , q = 1\n  , r = 1\n  , s = 1\n  , t = 1\n  }\n, field1 = T::{ a = 1 }\n}\n"
        Actual   (show): "{ field0 = T::{\n           , a = 1\n           , b = 1\n           , c = 1\n           , d = 1\n           , e = 1\n           , f = 1\n           , g = 1\n           , h = 1\n           , i = 1\n           , j = 1\n           , k = 1\n           , l = 1\n           , m = 1\n           , n = 1\n           , o = 1\n           , p = 1\n           , q = 1\n           , r = 1\n           , s = 1\n           , t = 1\n           }, field1 = T::{ a = 1 } }\n"
      ./tests/format/applicationMultiline:                                                                                     FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        { app =
            ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
              aaaaaaaaaaaaaaaaa
        , app2 =
            f
              a
              b
              cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
        , app3 =
            f
              a
              b
              c
              dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
        , some = Some
            aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        , merge =
            merge
              aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
              bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
        , merge3 =
            merge
              a
              b
              ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
        , toMap =
            toMap
              aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        }
        
        
        Actual:
        
        { app = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
                  aaaaaaaaaaaaaaaaa
        , app2 =
            f
              a
              b
              cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
        , app3 =
            f
              a
              b
              c
              dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
        , some = Some
            aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        , merge = merge
                    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
                    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
        , merge3 =
            merge
              a
              b
              ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
        , toMap =
            toMap
              aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        }
        
        
        Expected (show): "{ app =\n    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n      aaaaaaaaaaaaaaaaa\n, app2 =\n    f\n      a\n      b\n      cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\n, app3 =\n    f\n      a\n      b\n      c\n      dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\n, some = Some\n    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n, merge =\n    merge\n      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n      bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n, merge3 =\n    merge\n      a\n      b\n      ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\n, toMap =\n    toMap\n      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n}\n"
        Actual   (show): "{ app = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n          aaaaaaaaaaaaaaaaa\n, app2 =\n    f\n      a\n      b\n      cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\n, app3 =\n    f\n      a\n      b\n      c\n      dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\n, some = Some\n    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n, merge = merge\n            aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n            bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n, merge3 =\n    merge\n      a\n      b\n      ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\n, toMap =\n    toMap\n      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n}\n"
      ./tests/format/innerMultiline:                                                                                           FAIL
        tests/Dhall/Test/Format.hs:77:
        The formatted expression did not match the expected output
        Expected:
        
        { inner =
            ''
        
            one
        
            two
        
            three
        
            ''
        }
        
        
        Actual:
        
        { inner = ''
        
                  one
        
                  two
        
                  three
        
                  '' }
        
        
        Expected (show): "{ inner =\n    ''\n\n    one\n\n    two\n\n    three\n\n    ''\n}\n"
        Actual   (show): "{ inner = ''\n\n          one\n\n          two\n\n          three\n\n          '' }\n"
  lint tests
    discover
      ./tests/lint/success/optionalFoldBuild:                                                                                  FAIL
        tests/Dhall/Test/Lint.hs:64:
        The linted expression did not match the expected output
        expected: "{ example0 =\n      \955(a : Type)\n    \8594 \955(o : Optional a)\n    \8594 \955(optional : Type)\n    \8594 \955(some : a \8594 optional)\n    \8594 \955(none : optional)\n    \8594 merge { Some = some, None = none } o\n, example1 =\n      \955(a : Type)\n    \8594 \955 ( build\n        :   \8704(optional : Type)\n          \8594 \8704(some : a \8594 optional)\n          \8594 \8704(none : optional)\n          \8594 optional\n        )\n    \8594 build (Optional a) (\955(x : a) \8594 Some x) (None a)\n, example2 = merge { Some = Natural/even, None = False } (Some 1)\n}\n"
         but got: "{ example0 =   \955(a : Type)\n             \8594 \955(o : Optional a)\n             \8594 \955(optional : Type)\n             \8594 \955(some : a \8594 optional)\n             \8594 \955(none : optional)\n             \8594 merge\n                 { Some = some, None = none }\n                 o, example1 =   \955(a : Type)\n                               \8594 \955 ( build\n                                   :   \8704(optional : Type)\n                                     \8594 \8704(some : a \8594 optional)\n                                     \8594 \8704(none : optional)\n                                     \8594 optional\n                                   )\n                               \8594 build\n                                   (Optional a)\n                                   (\955(x : a) \8594 Some x)\n                                   (None a), example2 = merge\n                                                          { Some = Natural/even\n                                                          , None = False\n                                                          }\n                                                          (Some 1) }\n"
      ./tests/lint/success/multilet:                                                                                           FAIL
        tests/Dhall/Test/Lint.hs:64:
        The linted expression did not match the expected output
        expected: "-- example0.dhall\n\nlet Person\n    : Type\n    =   \8704(Person : Type)\n      \8594 \8704(MakePerson : { children : List Person, name : Text } \8594 Person)\n      \8594 Person\n\nlet example\n    : Person\n    =   \955(Person : Type)\n      \8594 \955(MakePerson : { children : List Person, name : Text } \8594 Person)\n      \8594 MakePerson\n          { children =\n            [ MakePerson { children = [] : List Person, name = \"Mary\" }\n            , MakePerson { children = [] : List Person, name = \"Jane\" }\n            ]\n          , name = \"John\"\n          }\n\nlet everybody\n    : Person \8594 List Text\n    = let concat = http://prelude.dhall-lang.org/List/concat\n\n      in    \955(x : Person)\n          \8594 x\n              (List Text)\n              (   \955(p : { children : List (List Text), name : Text })\n                \8594 [ p.name ] # concat Text p.children\n              )\n\nlet result\n    : List Text\n    = everybody example\n\nin  result\n"
         but got: "-- example0.dhall\n\nlet Person\n    : Type\n    =   \8704(Person : Type)\n      \8594 \8704(MakePerson : { children : List Person, name : Text } \8594 Person)\n      \8594 Person let example\n                   : Person\n                   =   \955(Person : Type)\n                     \8594 \955 ( MakePerson\n                         : { children : List Person, name : Text } \8594 Person\n                         )\n                     \8594 MakePerson\n                         { children = [ MakePerson { children = [] : List Person\n                                                   , name = \"Mary\"\n                                                   }\n                                      , MakePerson { children = [] : List Person\n                                                   , name = \"Jane\"\n                                                   }\n                                      ]\n                         , name = \"John\"\n                         } let everybody\n                               : Person \8594 List Text\n                               = let concat =\n                                       http://prelude.dhall-lang.org/List/concat\n\n                                 in    \955(x : Person)\n                                     \8594 x (List Text) (   \955 ( p\n                                                           : { children :\n                                                                 List ( List\n                                                                          Text\n                                                                      )\n                                                             , name : Text\n                                                             }\n                                                           )\n                                                       \8594   [ p.name ]\n                                                         # concat\n                                                             Text\n                                                             p.children\n                                                     ) let result\n                                                           : List Text\n                                                           = everybody\n                                                               example in result\n"
      ./tests/lint/success/issue1586:                                                                                          FAIL
        tests/Dhall/Test/Lint.hs:64:
        The linted expression did not match the expected output
        expected: "let {- 1 -} x\n    {- 2 -} : {- 3 -} Natural\n    = {- 4 -} 1\n\nin  x\n"
         but got: "let {- 1 -} x\n    {- 2 -} : {- 3 -} Natural\n    = {- 4 -} 1 in x\n"
      ./tests/lint/success/regression0:                                                                                        FAIL
        tests/Dhall/Test/Lint.hs:64:
        The linted expression did not match the expected output
        expected: "let replicate = https://prelude.dhall-lang.org/List/replicate\n\nin  replicate 10 Text \"!\"\n"
         but got: "let replicate = https://prelude.dhall-lang.org/List/replicate in replicate\n                                                                   10\n                                                                   Text\n                                                                   \"!\"\n"
      ./tests/lint/success/assert:                                                                                             FAIL
        tests/Dhall/Test/Lint.hs:64:
        The linted expression did not match the expected output
        expected: "let simpleAssert = assert : 1 + 1 \8801 2\n\nlet assertIn1Lam = \955(n : Natural) \8594 assert : Natural/subtract 0 n \8801 n\n\nlet assertIn2Lams =\n        \955(m : Natural)\n      \8594 \955(n : Natural)\n      \8594 assert : Natural/subtract m m \8801 Natural/subtract n n\n\nlet assertInLetInLam = \955(m : Natural) \8594 let n = m + 0 in assert : m \8801 n\n\nin  {=}\n"
         but got: "let simpleAssert = assert : 1 + 1 \8801 2\n\nlet assertIn1Lam = \955(n : Natural) \8594 assert : Natural/subtract 0 n \8801 n\n\nlet assertIn2Lams = \955(m : Natural) \8594 \955(n : Natural) \8594   assert\n                                                      :   Natural/subtract m m\n                                                        \8801 Natural/subtract n n\n\nlet assertInLetInLam = \955(m : Natural) \8594 let n = m + 0 in assert : m \8801 n\n\nin  {=}\n"

21 out of 1445 tests failed (7.72s)

Not all of them look obviously worse, but some do. For example:

        Expected:
        
        let x
            -- xxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx
            : Natural
            = 0
        
        let y
            -- yyyyyyyyyy yyyyyyyyyyyyyyyyy yyyyyyyyyyyyyyyyy
            -- yyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyy
            : Natural
            = 1
        
        in  x + y
        
        
        Actual:
        
        let x
            -- xxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx
            : Natural
            = 0 let y
                    -- yyyyyyyyyy yyyyyyyyyyyyyyyyy yyyyyyyyyyyyyyyyy
                    -- yyyyyyyyyyyyyy yyyyyyyyyyyyyyyyyyyyyyy
                    : Natural
                    = 1 in x + y

You can run these yourself if you want to give them a try. I use this patch to use my local prettyprinter version:

--- a/cabal.project
+++ b/cabal.project
@@ -1,4 +1,4 @@
-packages: ./dhall ./dhall-bash ./dhall-json ./dhall-yaml ./dhall-lsp-server ./dhall-nix
+packages: ./dhall ./dhall-bash ./dhall-json ./dhall-yaml ./dhall-lsp-server ../prettyprinter/prettyprinter ../prettyprinter/prettyprinter-ansi-terminal
 
 -- TODO: Remove this once hnix has a compatible release:
 -- https://github.com/haskell-nix/hnix/issues/524

The command I use is

cabal test dhall:tasty --test-options="--hide-successes"

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 22, 2020

Woo walls of errors ^^ I'll look through them some more :) Thanks for sharing.
Seems like it's eating up newlines in some situations. Too bad that the test suite in prettyprinter did not catch that.

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 22, 2020

Too bad that the test suite in prettyprinter did not catch that.

Yep. A more robust testsuite is the least that we ought to get out of this experiment! 👍

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

I could not get the dhall testsuite running (haven't tried much either because my property tests were failing in the kotlin project and that gave nice examples).

So this fails for several things:
Column, Nesting and PageWidth necessitate existance of Fail. This whole idea is based on the fact that you can somehow figure out if a branch will fail or not (which during flattening of any other component you can, and even relatively cheap!).

I eventually got it to pass tests, however not without complicating the whole thing, will translate it to haskell and push something tomorrow, don't have much time for it today...

So a few interesting points here:

  • FlatAlt can always be split into Union (flatten r) l.
  • group x@(Union _ _) = x works just fine.
  • Cat a b: Here is where the fun starts:
    • Cat a b where either a or b contans a Line cannot be flattened
    • Cat a b where a has no changes on flattening can safely nest Union on the right side. Cat a b = Cat a (group b) or something similar
    • same thing goes for b with no changes
    • in all other cases we need to create a union above the cat and include both sides there
  • if Column, Nesting and WithPageWidth are encountered within group, the group call can safely be pushed inside making the resulting document much easier to render if for example you introduce align at the very top of your document.
  • Fail is needed for cases where we cannot predict wether or not a branch of a Cat or FlatAlt will fail
  • Because a Fail is needed I end up needing 3 individual functions to to this: group which tries to create a union as far down as possible. groupNoLine which returns information about how this branch will render if at all and flatten. This is thus very similar to the old group except this one does more work pushing the Union further down. (And thus also takes away work from changesUponFlattening/groupNoLine.

So in conclusion I think this idea is worth exploring further because it produces very small documents and avoids Fail as much as possible. If I understand this correctly every step taken in this function would have also been taken by group before (just some of them delayed due to laziness, but the leftmost side is checked first in all cases and that has to be fully evaluated by all layout algorithms anyway).

For now if you want to take a look at the kotlin code: (I had to recreate all that debug stuff you made a while ago to even be able to work with on this ^^)
Ignore the Eval stuff, it's a method of introducing laziness to the jvm, just mentally remove all of it because the haskell code literally doesn't need it one bit^^
https://github.com/1Jajen1/kotlin-pretty/blob/group-test/kotlin-pretty/src/main/kotlin/pretty/Debug.kt

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

Column, Nesting and PageWidth necessitate existance of Fail. This whole idea is based on the fact that you can somehow figure out if a branch will fail or not (which during flattening of any other component you can, and even relatively cheap!).

Can you expand on this? I'd very much like to understand it!

I'd also like to understand how it fits together with this bit:

  • if Column, Nesting and WithPageWidth are encountered within group, the group call can safely be pushed inside making the resulting document much easier to render if for example you introduce align at the very top of your document.

Thanks for the link to your kotlin code – TBH it's rather difficult for me to read though. :/

the leftmost side is checked first in all cases and that has to be fully evaluated by all layout algorithms anyway

Note that layoutCompact ignores the left Union alternative! Although, if we can speed up layoutSmart and layoutPretty at the expense of making layoutCompact slower, that might still be a good trade-off.

@sjakobi sjakobi mentioned this pull request Jan 23, 2020
@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

On phone so text will be a little uglier, doing backticks etc is just too slow^^

Column, Nesting and PageWidth necessitate existance of Fail. This whole idea is based on the fact that you can somehow figure out if a branch will fail or not (which during flattening of any other component you can, and even relatively cheap!).

Can you expand on this? I'd very much like to understand it!

I'd also like to understand how it fits together with this bit:

  • if Column, Nesting and WithPageWidth are encountered within group, the group call can safely be pushed inside making the resulting document much easier to render if for example you introduce align at the very top of your document.

Sure, let's quickly reiterate how this whole thing works:
Withing group I am going as far down the Doc as possible to figure out where to place a Union. The only three cases we care about for branching are:

  • FlatAlt, which we can straight up replace in place with Union. We also need to further flatten the alternative part. When using flatten directly there are no more options, but when we use groupNoLine we get back information if the flattened part has a Line, in which case we can skip creating Union and directly use the non-flat part of FlatAlt. On both other cases (NoChange, and Flattened) we need to create a Union because the document has changed (and may contain Fail, but more on that later)
  • Union. At this point we haven't changed the document, so to hit a Union now means the work is already done, just return it.
  • Cat: Note, at this point there are still no changes to the upper levels of the Doc (because there did not need to be any (no FlatAlts). To flatten a Cat we need to flatten both sides and it's kind of important to keep track of how the sides flatten because that affects how efficient we can place the Union. If either side has a Line it cannot be flattened and no Union can be created. If one side is NoChange we can safely put the Union within the Cat on the other part, regardless of how that flattens. In the last other case we need to put the Union above the Cat, which is what group produced before.
  • Column, Nesting, WithPageWidth: We have not had to change the doc yet! So at this point we can safely assume that everything up to this point is as flat as possible. So we can move the Union inside (and maybe even much further down)
  • Nest, Annotated: Just recurse, because it does not matter if we have Union (Nest i x) (Nest i y) or Nest i (Union x y)
  • All other cases are terminal and thus don't need to be flattened, so leave the doc as is

Now onto the harder part: Checking what flattening results in: We get here from either FlatAlt or Cat.

  • FlatAlt x y: This is the one cases this doesn't handle nicely yet, nested FlatAlts... Currently I check y for its flatten result. If it has a Line we cannot flatten the whole thing so return HasLine all the way. In all other cases I currently create a and assume the content as Flattened. This could be optimized more I think
  • Union x _: A union is always Flattened x. This could have more thought put in as well, but I think it works for now
  • Cat again is fun: Again any HasLine means it cannot be flattened, just return HasLine. NoChange is irrelevant this time because we are not concerned about putting Unions in (that will be done a level higher by group with this result) so the only interesting case is if both sides are NoChange because then we can return NoChange. All other cases lead to Flattened and return the Cat with x and y flattened.
  • Line returns HasLine, indicates that we could not flatten
  • Nest, Annotated are just as before simply recursive calls
  • Column/Nesting/WithPageWidth: We expect some sort of FlattenResult, but we cannot know if the function produces a Line or not, so we assume the worst and return it as Flattened and call flatten recursively
  • All other cases are NoChange

The last part is flatten itself, but that is left unchanged from the current code.

Thanks for the link to your kotlin code – TBH it's rather difficult for me to read though. :/

Takes quite a bit of time to get used to ^^ Especially because this is by no means how one should write kotlin code, it's just hard to make a datatype like Doc stacksafe an efficient without it. (Before introducing all the Eval stuff it choked on garbage collection because the layouter had to fully build every doc into the full simpledoc every time)

the leftmost side is checked first in all cases and that has to be fully evaluated by all layout algorithms anyway

Note that layoutCompact ignores the left Union alternative! Although, if we can speed up layoutSmart and layoutPretty at the expense of making layoutCompact slower, that might still be a good trade-off.

True, I forgot! This will pose quite the penalty to it as it has to evaluate the rightmost path, evaluate all left paths. It is actually the only algorithm that benefits from having Union as high up as possible.

This can't really be seperate from group because it assumes existing Unions to be optimal already.

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

Thanks for your explanations! They make sense to me.

Unfortunately I still don't understand what you meant with

Column, Nesting and PageWidth necessitate existance of Fail.

But maybe I should just wait for you to fix this PR – the code might make it clearer. :)

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

Here's an idea for testing the new group: Keep the old version (or an even simpler version) as simpleGroup in Internal and test that any document using group produces the same SimpleDocStream (or Text) as the equivalent document using simpleGroup.

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

Thats actually what I did ^-^
Works fine until you need a decent counterexample because shrinking is so poor.

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

Put the updated thing on the branch. This fails 2 doctests for reflow, will check those later today or tomorrow. Interestingly the property group == simpleGroup seems to hold... I'll configure it to run more tests to be sure!

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

I'll configure it to run more tests to be sure!

And there it goes and fails straight away after just 19 tests with a huge counterexample...

Edit: Nvm fixed, now its just back to doctests failing with imports missing.

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

Nice! :)

Could you possibly address the CI failures (mostly HLint I believe)?

Copy link
Collaborator

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! That makes a lot of sense to me! 👍

Could you possibly collect some examples (with Diag) that demonstrate what group does differently than the old simpleGroup?

I'll try to figure out the performance impact.


Column f -> Flat (Column (flatten . f))
Nesting f -> Flat (Nesting (flatten . f))
WithPageWidth f -> Flat (WithPageWidth (flatten . f))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why do we need to use flatten instead of group here? I think you tried to explain that before but I haven't grokked it yet. ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because changesOnFlattening is expected to return a value that tells the caller if the doc changed (either by flattening, by encountering a Line or not changing). And you cannot get that information out of a function :/

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I don't understand what you're saying here. Both flatten and group return Docs. And group can actually add information by wrapping things in Union.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats what you are thinking about! Yes definitly that is a good idea, I'll test that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails quickcheck with a huge counterexample again and my own tests with a small one. Now to figure out whats wrong...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counterexample: column (\_ -> hardline). This flattens to column (\_ -> Fail) but when using group to column (\_ -> Line).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's very interesting! Would that explain the test failures I got for the first version of this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, or at least the problem was very similar iirc.

x@Char{} -> x
x@Text{} -> x

data FlatteningResult a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this type have different semantics than FlattenResult, or what's the reason for adding it?

If you don't like the constructor names on FlattenResult, feel free to change them. (I might bikeshed your suggestions a bit though. ;))

Copy link
Contributor Author

@1Jajen1 1Jajen1 Jan 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I just added names this way to not conflict with the others. No technically you could the other type, just wanted to make a clear distinction, if this ends up performing much better than master and you want to merge it, I can change that 👍

groupedLayedOut = layout layouter grouped
groupedSimpleLayedOut = layout layouter groupedSimple
in counterexample ("Grouped: " ++ (show . diag) (grouped))
(counterexample ("Grouped (Simple) " ++ (show . diag) (groupedSimple))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nested counterexample is a bit surprising IMHO – I'd prefer something similar to mkCounterexample above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ Agree, just took a shortcut tbh ;)

Copy link
Collaborator

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more linter fixes.

prettyprinter/test/Testsuite/Main.hs Outdated Show resolved Hide resolved
prettyprinter/test/Testsuite/Main.hs Outdated Show resolved Hide resolved
1Jajen1 and others added 2 commits January 23, 2020 22:32
Co-Authored-By: Simon Jakobi <simon.jakobi@gmail.com>
Co-Authored-By: Simon Jakobi <simon.jakobi@gmail.com>
@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

In my dhall benchmark this performs substantially slower than master – it's a bit hard to say how much, because there are other expensive things going on too.

I'll check the benchmarks in this package too (or you can if you want).

What I suspect might be the reason, is that by pushing the Union constructors deeper into the document, we create more work for the next group iteration: The next group will have to check more already flattened stuff before encountering a Union where it can finally stop.

I have recorded one idea to counter this issue in #123 (comment). What happens when we wrap each group result in a Union Fail ... to mark it as flattened? Or maybe that Fail would wreak havok. In that case we could try Union x x for a group result x.

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

In my dhall benchmark this performs substantially slower than master – it's a bit hard to say how much, because there are other expensive things going on too.

I'll check the benchmarks in this package too (or you can if you want).

What I suspect might be the reason, is that by pushing the Union constructors deeper into the document, we create more work for the next group iteration: The next group will have to check more already flattened stuff before encountering a Union where it can finally stop.

This makes sense. This group implementation tries it's hardest to not produce branches, or produce very small branches, so subsequent group calls will suffer!

I have recorded one idea to counter this issue in #123 (comment). What happens when we wrap each group result in a Union Fail ... to mark it as flattened? Or maybe that Fail would wreak havok. In that case we could try Union x x for a group result x.

Putting it into Union Fail x is fine, Union x x should also work, will try both

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

In that case we could try Union x x for a group result x.

Hm, that might cause more work for the layouters… If we can't find another marker, we could consider adding a dedicated constructor Flat.

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

In that case we could try Union x x for a group result x.

Hm, that might cause more work for the layouters… If we can't find another marker, we could consider adding a dedicated constructor Flat.

That was also initially my thought, this will happen if x evaluates to Fail. So Union Fail x is the better choice there

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

That was also initially my thought, this will happen if x evaluates to Fail. So Union Fail x is the better choice there

Ah another case of doctests fail and the property tests succeed 😱

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

That was also initially my thought, this will happen if x evaluates to Fail. So Union Fail x is the better choice there

Ah another case of doctests fail and the property tests succeed

In Internal.Debug? Sorry, I'm responsible for those. Will fix! I thought CI would check them…

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

That was also initially my thought, this will happen if x evaluates to Fail. So Union Fail x is the better choice there

Ah another case of doctests fail and the property tests succeed

In Internal.Debug. Sorry, I'm responsible for those. Will fix! I thought CI would check them…

Those are the import errors in the doctests. Theres far more things failing by inserting a Union Fail (group_ x) at the top ^^

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 23, 2020

Theres far more things failing by inserting a Union Fail (group_ x) at the top ^^

Can you post them here?

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

prettyprinter                       > src/Data/Text/Prettyprint/Doc/Util.hs:20: failure in expression `putDoc (tupled (words "Lorem ipsum dolor"))'
prettyprinter                       > expected: (Lorem, ipsum, dolor)
prettyprinter                       >  but got: (Lorem, ipsum, dolor )

They are all pretty much the same:
There were a lot more failing, which no longer do for some weird reason (don't think I did anything). Either way I can reproduce this and I think I might have an idea...

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

This is to blame:

It incorrectly flattens Union Fail _ to Fail... Using Union to tag flattened docs makes some stuff quite hard to reason about...

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 23, 2020

Either way, I am out for today, will check on this tomorrow again^^

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 24, 2020

So let's reiterate a bit, because I think this approach is flawed:

  • Group x works by creating a Union with the left side flattened. This flat left side should never contain more Union's or FlatAlt's because that would mean it is not the flattest there is!
  • changesUponFlattening takes the burdon away from the layouter and shorts early on to not create a union
  • This pr's version of group takes this one step further to create the union as deep as possible to further avoid a layouter having to go through the same parts of a document over and over again
  • This approach in general is not wrong and produces really small Doc's however this comes at a cost when repeating group because the next group has to traverse up untill where the Union was placed (if at all). When it reaches no union, it too will place no Union at the top, when it does reach a Union it has to take the left side, backtrack and select the right place to place a Union.
  • changesUponFlattening in current master does the same thing except that the union is higher up so the impact isn't as great. (It still needs to retraverse the entire document to repeatedly come to the NoChanges, Unflattenable conclusion, which I suspect is the main cost when having multiple groups!)

So how would one improve this:

  • Keep track of a document being able to flatten or not in the constructors somehow: Specifically Cat which is the only constructor where we have to recurse through both sides to know for sure.

If Cat knew either of it's contents were flat we would avoid a lot of work in both this pr and changesOnFlattening. All other constructors are not so important (Nest and Annotated are the only other recursive ones that matter for this question and I suspect they don't see quite as much use as Cat)

Ideas/thoughts?

Either way this experiment helped me understand how flattening works on a whole new level, so I'd already call it a win ^^

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 24, 2020

Thanks for the great summary, @1Jajen1! :) I think we should eventually put most of it into a note in the code.

Regarding the idea to keep track of what's flat, I think the obvious solution is to add a new constructor Flat (Doc ann) or Grouped (Doc ann) that gets wrapped around any group result or just those that don't produce a Union.

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Jan 27, 2020

I'll play around with adding a Flat constructor to speed up the AlreadyFlat and NeverFlat cases in group in a week or two (too many other things atm). I'll also close this then, because I think the benefits of this (the layouter's are much faster) vs the downsides (repeated groups suffer) are not worth it, although I'll probably add a Flat constructor to both so it can be compared. This is a good reference for future optimization around group though ^^

@sjakobi
Copy link
Collaborator

sjakobi commented Jan 27, 2020

Sounds good to me, @1Jajen1! 👍

Let's also make sure that any lessons learned make it into documentation in the code where I think there's the highest probability that they will be preserved.

BTW note that I've done a more conservative experiment with group at #140, although with good results so far. I haven't checked how much layoutCompact is impacted though!

@1Jajen1
Copy link
Contributor Author

1Jajen1 commented Mar 24, 2021

Been a while 😅

I did some work on the kotlin version and remembered this attempt and that I still have this note to write 🙈

Also when thinking about this again, this left me somewhat unsatisfied as this seems like an approach that could lead to better performance. Has anything changed on the benchmark side, that makes it a bit easier to just play around with a few ideas?

Copy link
Collaborator

@sjakobi sjakobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on this thread, Jannis! :)

Unfortunately our whole conversation is paged out for me now – which hopefully means that some of my questions will be easy to answer! 😅

Has anything changed on the benchmark side, that makes it a bit easier to just play around with a few ideas?

Unfortunately not. Maybe I should just bite the bullet and add a little benchmark package to this repo that depends on dhall.

@@ -630,6 +630,24 @@ group x = case x of
-- See https://github.com/quchen/prettyprinter/issues/22 for the corresponding
-- ticket.

-- Note [Group: Optimial placement of Union and loss of information]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- Note [Group: Optimial placement of Union and loss of information]
-- Note [Group: Optimal placement of Union and loss of information]

@@ -630,6 +630,24 @@ group x = case x of
-- See https://github.com/quchen/prettyprinter/issues/22 for the corresponding
-- ticket.

-- Note [Group: Optimial placement of Union and loss of information]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a reference to this note next to the other note reference in group?

Comment on lines 641 to 642
-- This approach would not increase the cost of a call to group at all as
-- changesUponFlattening already traverses deep enough already, however, due to
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't very obvious. Wouldn't the new approach increase the "length" of the Doc that we need to rebuild on top of the new Union?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I forget the cost of recreating the Doc, that whole line is guesswork anyway and should probably be removed. I was only thinking in traversal depth.

Comment on lines 642 to 649
-- changesUponFlattening already traverses deep enough already, however, due to
-- an unrelated property of group, placing the resulting 'Union' further down or
-- not at all will harm subsequent calls to group. Currently when calling group
-- with a document that cannot be flattened or an already flat document the Doc
-- will not change at all. This looses information as subsequent calls to group
-- have to come to the same conclusion again and thus retraverse. If we now
-- place the Union (the only evidence that we have done some work) deeper in the
-- tree, subsequent calls will have to retraverse even more nodes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this extra work / loss of information be avoided by adding another Union Fail (or a new dedicated marker) on top of the result of group?

Copy link
Contributor Author

@1Jajen1 1Jajen1 Mar 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was thinking as well. Even right now, without placing Unions deeper) doing so would avoid re-traversing failing or already flat documents. This + placing unions at the optimal depth could be interesting to play around with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants