packages feed

egison-5.1.0: hs-src/Language/Egison/Type/Subtype.hs

{- |
Module      : Language.Egison.Type.Subtype
Licence     : MIT

Phase beta of the extensible CAS tower (design/type-cas-tower.md D1/D5,
design/type-cas-tower-implementation.md section 3).

This module implements the dynamic subtype order: a structural skeleton
(the design document's inclusion table, complete with coefficient
propagation) extended with user-declared `declare cas-subtype A ⊂ B`
edges, plus the D1 declare-time join-semilattice check:

  * an edge already derivable from the order is redundant (warning; it is
    still stored so its endpoints participate in later checks)
  * an edge whose reverse already holds would collapse the order (error)
  * an edge that leaves some pair of nodes with two incomparable minimal
    upper bounds is ambiguous (error, with suggested completing edges)
  * an edge that places the target strictly below an existing join
    refines that join (warning; refinement monotonicity — values never
    change, static types only become more precise)

The pair enumeration runs over declared nodes (all endpoints ever
declared, including redundant edges) — a finite approximation of the
full scheme-level check, to be refined counterexample-driven
(implementation plan, section 7).

This order also backs instance resolution (Type.Instance and Core's
runtime dispatch) through 'isSubtypeWith', which additionally treats
'TAny' as the top element for arbitrary (non-CAS) instance types. The
older, partial relation that instance resolution used to carry
separately ('Type.Join.isSubtype') has been retired.
-}

module Language.Egison.Type.Subtype
  ( SubtypeEdge
  , SubtypeEnv
  , isCasType
  , symbolSetSubset
  , sameCasHead
  , skeletonSubtype
  , skeletonJoin
  , isSubtypeWith
  , joinTypesWith
  , EdgeCheck(..)
  , checkEdgeAddition
  ) where

import           Data.List                  (nub)

import           Language.Egison.Type.Types (SymbolSet (..), Type (..))

-- | A declared subtype edge (lhs ⊂ rhs), with cas-type aliases expanded.
type SubtypeEdge = (Type, Type)

-- | All declared edges, in declaration order (redundant ones included).
type SubtypeEnv = [SubtypeEdge]

-- | The types the CAS order talks about. Everything else only relates
-- to itself (reflexivity).
isCasType :: Type -> Bool
isCasType TInt         = True
isCasType TMathValue   = True
isCasType TFactor      = True
isCasType (TTerm t _)  = isCasType t
isCasType (TPoly t _)  = isCasType t
isCasType (TFrac t)    = isCasType t
isCasType _            = False

-- | Same top-level CAS constructor. Instance dispatch uses this to break
-- ties among order-EQUIVALENT most-specific candidates: with the complete
-- skeleton, types like Poly MathValue [..] and Frac MathValue embed into
-- each other (constant embedding one way, numerator embedding the other —
-- only possible at the MathValue-coefficient level, where both denote the
-- full value domain). Order-equivalent types are the same value set in
-- different canonical forms, so specificity cannot decide between them;
-- per the design principle (types select representations), dispatch
-- follows the target's own representation head.
sameCasHead :: Type -> Type -> Bool
sameCasHead (TPoly _ _) (TPoly _ _) = True
sameCasHead (TTerm _ _) (TTerm _ _) = True
sameCasHead (TFrac _)   (TFrac _)   = True
sameCasHead a b = a == b

-- | Check if one symbol set is a subset of another.
symbolSetSubset :: SymbolSet -> SymbolSet -> Bool
symbolSetSubset _ SymbolSetOpen = True  -- Everything is subset of open
symbolSetSubset SymbolSetOpen (SymbolSetClosed _) = False  -- Open not subset of closed
symbolSetSubset (SymbolSetClosed s1) (SymbolSetClosed s2) = all (`elem` s2) s1
symbolSetSubset (SymbolSetVar _) _ = True  -- Assume vars are subset (will be resolved)
symbolSetSubset _ (SymbolSetVar _) = True

--------------------------------------------------------------------------------
-- Structural skeleton
--------------------------------------------------------------------------------

-- | The structural skeleton subtype relation: the design document's
-- inclusion table (type-cas.md 型の包含関係) with its propagation rules,
-- applied recursively through coefficients:
--
--   * Integer embeds into Factor and into any Frac/Poly/Term whose
--     coefficient admits Integer
--   * Factor embeds where Integer-coefficient polys live
--   * Frac a embeds into Poly b s when Frac a embeds into the
--     coefficient b (constant embedding, e.g. Frac Integer ⊂ Poly (Frac
--     Integer) s)
--   * Term/Poly are covariant in the coefficient with atom-set inclusion
--   * Poly/Term a s ⊂ Frac b when they embed into b (numerator embedding)
--
-- Deliberately NOT included: relating nested and flat canonical forms
-- (e.g. Poly (Poly Integer [i]) [x] vs Poly Integer [i, x]). Those are
-- different canonical forms of the same value set; placing them in the
-- order is exactly what user-declared edges are for (usecase 04/08).
skeletonSubtype :: Type -> Type -> Bool
skeletonSubtype a b | a == b = True
skeletonSubtype a TMathValue = isCasType a
-- Integer
skeletonSubtype TInt TFactor     = True
skeletonSubtype TInt (TFrac t)   = t == TInt || skeletonSubtype TInt t
skeletonSubtype TInt (TPoly t _) = t == TInt || skeletonSubtype TInt t
skeletonSubtype TInt (TTerm t _) = t == TInt || skeletonSubtype TInt t
-- Factor (atomic element; lives wherever Integer coefficients do)
skeletonSubtype TFactor (TPoly t _) = t == TInt || skeletonSubtype TInt t
skeletonSubtype TFactor (TTerm t _) = t == TInt || skeletonSubtype TInt t
skeletonSubtype TFactor (TFrac t)   = skeletonSubtype TFactor t
-- Term
skeletonSubtype (TTerm t1 s1) (TTerm t2 s2) =
  skeletonSubtype t1 t2 && symbolSetSubset s1 s2
skeletonSubtype (TTerm t1 s1) (TPoly t2 s2) =
  skeletonSubtype t1 t2 && symbolSetSubset s1 s2
skeletonSubtype (TTerm t1 s1) (TFrac t2) = skeletonSubtype (TTerm t1 s1) t2
-- Poly
skeletonSubtype (TPoly t1 s1) (TPoly t2 s2) =
  skeletonSubtype t1 t2 && symbolSetSubset s1 s2
skeletonSubtype (TPoly t1 s1) (TFrac t2) = skeletonSubtype (TPoly t1 s1) t2
-- Frac
skeletonSubtype (TFrac t1) (TFrac t2)   = skeletonSubtype t1 t2
skeletonSubtype (TFrac t1) (TPoly t2 _) = skeletonSubtype (TFrac t1) t2
skeletonSubtype (TFrac t1) (TTerm t2 _) = skeletonSubtype (TFrac t1) t2
skeletonSubtype _ _ = False

-- | Skeleton join, following the design join table (type-cas.md
-- join の計算規則), including the documented tower rule
-- level 2 ⊔ level 3 = level 4:
--
--   join(Poly a s, Frac X) = Poly (join a (Frac X)) s   -- X non-Poly
--   join(Poly a s, Frac (Poly b s')) = Frac (Poly (join a b) (s ∪ s'))
--
-- (The legacy 'Type.Join.joinTypes', which returned level 5 for the first
-- case in disagreement with the design, had no callers and was removed.)
skeletonJoin :: Type -> Type -> Maybe Type
skeletonJoin a b
  | not (isCasType a) || not (isCasType b) = Nothing
  | skeletonSubtype a b = Just b
  | skeletonSubtype b a = Just a
skeletonJoin (TTerm t1 s1) (TTerm t2 s2) =
  TTerm <$> skeletonJoin t1 t2 <*> unionSymbolSets s1 s2
skeletonJoin (TTerm t1 s1) p@(TPoly _ _) = skeletonJoin (TPoly t1 s1) p
skeletonJoin p@(TPoly _ _) (TTerm t2 s2) = skeletonJoin p (TPoly t2 s2)
skeletonJoin (TTerm t1 s1) f@(TFrac _) = skeletonJoin (TPoly t1 s1) f
skeletonJoin f@(TFrac _) (TTerm t2 s2) = skeletonJoin f (TPoly t2 s2)
skeletonJoin (TPoly t1 s1) (TPoly t2 s2) =
  TPoly <$> skeletonJoin t1 t2 <*> unionSymbolSets s1 s2
skeletonJoin (TPoly t1 s1) (TFrac t2) = joinPolyFrac t1 s1 t2
skeletonJoin (TFrac t1) (TPoly t2 s2) = joinPolyFrac t2 s2 t1
skeletonJoin (TFrac t1) (TFrac t2) = TFrac <$> skeletonJoin t1 t2
skeletonJoin _ _ = Nothing

-- | join(Poly a s, Frac x): level 4 when x is a scalar domain, level 5
-- when x is itself a Poly (rational-function field).
joinPolyFrac :: Type -> SymbolSet -> Type -> Maybe Type
joinPolyFrac a s (TPoly b s') =
  TFrac <$> (TPoly <$> skeletonJoin a b <*> unionSymbolSets s s')
joinPolyFrac a s x = do
  coeff <- skeletonJoin a (TFrac x)
  return (TPoly coeff s)

-- | Union of atom sets: open absorbs, closed sets take the ordered union.
unionSymbolSets :: SymbolSet -> SymbolSet -> Maybe SymbolSet
unionSymbolSets SymbolSetOpen _ = Just SymbolSetOpen
unionSymbolSets _ SymbolSetOpen = Just SymbolSetOpen
unionSymbolSets (SymbolSetClosed s1) (SymbolSetClosed s2) =
  Just (SymbolSetClosed (nub (s1 ++ s2)))
unionSymbolSets (SymbolSetVar v1) (SymbolSetVar v2)
  | v1 == v2 = Just (SymbolSetVar v1)
  | otherwise = Just SymbolSetOpen
unionSymbolSets (SymbolSetVar _) ss = Just ss
unionSymbolSets ss (SymbolSetVar _) = Just ss

--------------------------------------------------------------------------------
-- Order with declared edges
--------------------------------------------------------------------------------

-- | Subtype in the declared order: skeleton, or a path that climbs
-- through declared edges (entering an edge from anything skeleton-below
-- its source). Worklist over edge targets; terminates because the
-- visited set only grows within the finite edge-target set.
-- TAny is the top element (instance resolution compares arbitrary
-- instance types through this relation, and TAny-typed heads accept
-- everything; TAny never occurs in declared CAS edges).
isSubtypeWith :: SubtypeEnv -> Type -> Type -> Bool
isSubtypeWith _ _ TAny = True
isSubtypeWith edges a b = go [a] [a]
  where
    go [] _ = False
    go (t:rest) visited
      | skeletonSubtype t b = True
      | otherwise =
          let nexts = [ y | (x, y) <- edges
                          , skeletonSubtype t x || t == x
                          , y `notElem` visited ]
          in go (rest ++ nexts) (visited ++ nexts)

-- | Join in the declared order: the unique minimal upper bound among the
-- skeleton join and the declared nodes. Ambiguity should be prevented by
-- the declare-time check; if it happens anyway we prefer the skeleton
-- join, and give up otherwise.
joinTypesWith :: SubtypeEnv -> Type -> Type -> Maybe Type
joinTypesWith edges a b =
  case minimals of
    [j] -> Just j
    []  -> Nothing
    js  -> case [ j | j <- js, Just j == skel ] of
             [j] -> Just j
             _   -> Nothing
  where
    skel = skeletonJoin a b
    nodes = nub (concatMap (\(x, y) -> [x, y]) edges)
    uppers = nub ( [ j | Just j <- [skel] ]
                ++ [ n | n <- nodes
                       , isSubtypeWith edges a n
                       , isSubtypeWith edges b n ] )
    minimals = [ u | u <- uppers, not (any (strictlyBelow u) uppers) ]
    strictlyBelow u v =
      v /= u && isSubtypeWith edges v u && not (isSubtypeWith edges u v)

--------------------------------------------------------------------------------
-- D1 declare-time check
--------------------------------------------------------------------------------

-- | Result of checking a new edge against the current order.
data EdgeCheck
  = EdgeRedundant
    -- ^ Already derivable; harmless (store it, warn).
  | EdgeCycle
    -- ^ The reverse relation already holds; adding the edge would
    -- collapse the two types into one order point (rejected).
  | EdgeAmbiguous [(Type, Type, Type)]
    -- ^ Pairs (x, y, oldJoin) whose minimal upper bounds would split
    -- into {oldJoin, target}; suggest declaring oldJoin ⊂ target.
  | EdgeRefines [(Type, Type, Type)]
    -- ^ Pairs (x, y, oldJoin) whose join refines from oldJoin down to
    -- the target (accepted with a warning).
  | EdgeOk
  deriving (Eq, Show)

-- | D1 join-semilattice check for adding edge (a ⊂ b).
-- Pairs are drawn from declared nodes below b in the extended order.
checkEdgeAddition :: SubtypeEnv -> SubtypeEdge -> EdgeCheck
checkEdgeAddition edges (a, b)
  | isSubtypeWith edges a b = EdgeRedundant
  | isSubtypeWith edges b a = EdgeCycle
  | not (null ambiguous)    = EdgeAmbiguous ambiguous
  | not (null refines)      = EdgeRefines refines
  | otherwise               = EdgeOk
  where
    edges' = (a, b) : edges
    nodes = nub ([a, b] ++ concatMap (\(x, y) -> [x, y]) edges)
    below = [ n | n <- nodes, n /= b, isSubtypeWith edges' n b ]
    pairs = [ (x, y) | x <- below, y <- below, x < y ]
    classified = [ (p, c) | p <- pairs, Just c <- [classify p] ]
    ambiguous = [ (x, y, j) | ((x, y), Right j) <- classified ]
    refines   = [ (x, y, j) | ((x, y), Left j)  <- classified ]
    -- Right = ambiguous (old join incomparable with b),
    -- Left  = refinement (b strictly below old join).
    classify (x, y) =
      case joinTypesWith edges x y of
        Nothing -> Nothing            -- no old join: b becomes one (fine)
        Just j
          | isSubtypeWith edges' j b -> Nothing   -- old join stays minimal
          | isSubtypeWith edges' b j -> Just (Left j)
          | otherwise                -> Just (Right j)