content: {-# LANGUAGE DerivingStrategies #-}
  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  {-# LANGUAGE OverloadedStrings #-}
  
  -- | This module defines the types and conversions from PureScript/CoreFn identifiers to Nix identifiers.
  -- This can be a tricky problem, since all three languages have different rules for what is and isn't allowed in certain kinds of identifiers.
  -- The goal of this module is to provide an API that takes care of all those concerns.
  -- In other words, the types in this module are as close to ready-to-print as possible.
  --
  -- The reasoning is that 'Key' and 'Var' are intended to be used in 'Expr'.
  -- 'Expr' should represent a _valid_ Nix expression, and therefore 'Key' and 'Var' need to represent valid keys and binders, respectively.
  -- We don't have to care about rejecting nonsensical values during printing.
  --
  -- Nix's rules for naked (non-quoted) identifiers are strictly more lenient than PureScripts (see links to specifications below), since nix allows '-' in identifiers (they cannot start with a '-' though).
  -- Unfortunately, that doesn't mean we can just naively convert identifiers, for example:
  --  - generated identifiers such as dictionary names in CoreFn can contain '$'
  --  - module names can contain '.'
  --  - keys might be quoted
  --  - we shouldn't shadow Nix keywords, especially those that aren't also PureScript keywords
  --  - we reserve any identifier starting with two leading underscores
  --
  -- Purescript identifiers:
  -- https://github.com/purescript/purescript/blob/master/lib/purescript-cst/src/Language/PureScript/CST/Lexer.hs#L689
  -- Nix identifiers:
  -- https://github.com/cstrahan/tree-sitter-nix/blob/master/src/grammar.json#L19
  module PureNix.Identifiers
    ( Var (..),
      mkVar,
      numberedVars,
      Key (..),
      identKey,
      stringKey,
      moduleKey,
      binderKey,
      numberedKeys,
    )
  where
  
  import Data.Maybe
  import Data.Set (Set)
  import qualified Data.Set as Set
  import Data.String
  import Data.Text (Text)
  import qualified Data.Text as T
  import qualified Language.PureScript as PS
  import qualified Language.PureScript.PSString as PS
  
  -- TODO rename to Binder, since this can occur in the LHS of a let-binding
  
  -- | A valid (i.e. containing no illegal characters) variable binder.
  -- Primarily constructed using 'mkVar'.
  newtype Var = UnsafeVar {unVar :: Text}
    deriving newtype (IsString, Eq, Show)
  
  identToText :: PS.Ident -> Text
  identToText (PS.Ident t) = t
  -- GenIdent is only used in PureScript for "unnamed" instances.
  -- Originally, in PureScript, all instances needed to be named:
  -- https://github.com/purescript/documentation/blob/master/language/Differences-from-Haskell.md#named-instances
  -- This was relaxed in 0.14.2:
  -- https://github.com/purescript/purescript/pull/4096
  identToText (PS.GenIdent mvar n) = fromMaybe "__instance" mvar <> T.pack (show n)
  identToText PS.UnusedIdent = "_"
  identToText (PS.InternalIdent _) = error "impossible"
  
  -- | Make a Nix variable binder from a CoreFn binder.
  --
  -- If a binder is a Nix keyword, we tick the binder with a hyphen.
  -- Since PureScript does not allow binders to contain hyphens, this should be safe.
  --
  -- Additionally, CoreFn can put dollar signs in generated names.
  -- We simply drop leading dollar signs, and the rest we convert to hyphens.
  mkVar :: PS.Ident -> Var
  mkVar = UnsafeVar . removeDollarSigns . tickKeywords . identToText
    where
      tickKeywords w
        | Set.member w keywords || T.isPrefixOf "__" w = w <> "-"
        | otherwise = w
      removeDollarSigns w =
        T.map (\c -> if c == '$' then '-' else c) $
          if T.isPrefixOf "$" w then T.tail w else w
  
  keywords :: Set Text
  keywords = Set.fromList (purenixIdents <> nixPrimops <> nixKeywords)
    where
      purenixIdents = ["module", "foreign"]
      -- These are not keywords in the sense that they can be shadowed
      -- For example, let true = false; in true is valid Nix.
      nixPrimops = ["builtins", "import", "false", "true"]
      -- keywords in nix:
      -- https://github.com/NixOS/nix/blob/90b2dd570cbd8313a8cf45b3cf66ddef2bb06e07/src/libexpr/lexer.l#L115-L124
      nixKeywords :: [Text]
      nixKeywords =
        ["if", "then", "else", "assert", "with", "let", "in", "rec", "inherit", "or"]
  
  -- | A valid Nix attribute key
  newtype Key = UnsafeKey {unKey :: Text}
    deriving newtype (IsString, Eq, Show)
  
  moduleKey :: PS.ModuleName -> Key
  moduleKey (PS.ModuleName mdl) = UnsafeKey $ "\"" <> mdl <> "\""
  
  identKey :: PS.Ident -> Key
  identKey = UnsafeKey . unVar . mkVar
  
  stringKey :: PS.PSString -> Key
  stringKey = UnsafeKey . PS.prettyPrintObjectKey
  
  binderKey :: Var -> Key
  binderKey = UnsafeKey . unVar
  
  numberedText :: Text -> [Text]
  numberedText prefix = fmap (\n -> prefix <> T.pack (show n)) [0 :: Int ..]
  
  numberedVars :: Text -> [Var]
  numberedVars = fmap UnsafeVar . numberedText
  
  numberedKeys :: Text -> [Key]
  numberedKeys = fmap UnsafeKey . numberedText
  