-- | This module provides a way to generate infinitely many distinct
-- concrete variable names from a list of name suggestions.
--
-- Since bound atoms must sometimes be renamed, we need a way to
-- generate suitable concrete names.  This should be configurable on a
-- per-atom basis, with a fallback default behavior for each atom
-- type.
--
-- A name suggestion is a list of possible names. The first useable
-- name from the list is chosen. If the list is finite and contains no
-- useable names, then we will generate more names by appending
-- numerical subscripts. A client can override this default behavior
-- by specifying an infinite list of name suggestions.
--
-- An empty list of name suggestions counts as no suggestion, in which
-- case a default will be used.
--
-- When merging two atoms (see 'Nominal.merge'), we concatenate their
-- name suggestions. In particular, if one name has a user-specified
-- suggestion and the other one does not, we always use the
-- user-specified one.

module Nominal.ConcreteNames where

import Data.Char
import Data.List
import Data.Set (Set)
import qualified Data.Set as Set

-- | A name suggestion is a list of possible names. When an atom must
-- be renamed, the first useable name from the list is chosen. If the
-- list is finite and contains no useable names, then additional names
-- will be generated by appending numerical subscripts. To override
-- this behavior, redefine 'Nominal.expand_names' for the given
-- 'AtomKind' instance, or specify an infinite list of names.
type NameSuggestion = [String]

-- | The names to use if nothing else was specified.
default_names :: NameSuggestion
default_names = ["x", "y", "z", "u", "v", "w", "r", "s", "t", "p", "q"]

-- | Convert a digit to a subscript.
to_subscript :: Char -> Char
to_subscript '0' = '₀'
to_subscript '1' = '₁'
to_subscript '2' = '₂'
to_subscript '3' = '₃'
to_subscript '4' = '₄'
to_subscript '5' = '₅'
to_subscript '6' = '₆'
to_subscript '7' = '₇'
to_subscript '8' = '₈'
to_subscript '9' = '₉'
to_subscript c = c

-- | Check if a character is a letter or underscore.
isAlphaOrWild :: Char -> Bool
isAlphaOrWild c = isAlpha c || c == '_'

-- | Generate an infinite list of possible names from a (possibly
-- finite) list of suggestions.
expand_default :: NameSuggestion -> [String]
expand_default xs0 = xs1 ++ xs3 ++ [ x ++ map to_subscript (show n) | n <- [1..], x <- xs3 ]
  where
    xs1 = [ x | x <- xs0, x /= "" ]
    xs2 = [ y | x <- xs0, let y = takeWhile isAlphaOrWild x, y /= "" ]
    xs3 = if xs2 == [] then default_names else xs2

-- | A name generator consists of a (possibly finite) list of name
-- suggestions and an /expander/, which is a function for generating
-- more names.
data NameGen = NameGen NameSuggestion (NameSuggestion -> [String])

-- | The default name generator.
default_namegen :: NameGen
default_namegen = NameGen default_names expand_default

-- | Compute a string that is not in the given set, and whose name is
-- based on the supplied suggestions.
rename_fresh :: Set String -> NameGen -> String
rename_fresh as (NameGen ns expander) = n'
  where
    n' = head [ x | x <- expand_default (ns' ++ expander ns'), not (used x) ]
    ns' = if null ns then default_names else ns
    used x = x `Set.member` as

-- | Merge two name suggestions. Essentially this appends them, but we
-- try to avoid duplication. We use the left expander.
combine_names :: NameGen -> NameGen -> NameGen
combine_names (NameGen xs ex1) (NameGen ys ex2) = NameGen (xs ++ (ys \\ xs)) ex1