purenix-1.0: src/PureNix/Identifiers.hs
{-# 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 = 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