packages feed

ychr-0.1.0.0: src/YCHR/Internal/Resolved.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Resolved AST
--
-- This module defines the AST produced by the resolve phase, which sits
-- between renaming and desugaring. It is the result of flattening all
-- modules into a single program and grouping function equations under
-- their declarations.
--
-- Key properties that hold by construction:
--
--   * Function equations live inside their 'FunctionDef', so there is
--     no way for a constraint-declared name to have equations.
--
--   * Rule heads are verified during resolution: no function-declared
--     name can appear in a rule head.
--
--   * Constraint head names and function names are 'QualifiedName',
--     so the qualification invariant established by the renamer is
--     reflected in the type system.
module YCHR.Internal.Resolved
  ( -- * Types
    Program (..),
    Rule (..),
    Head (..),
    FunctionDef (..),
    FunctionEquation (..),
    Expr (..),

    -- * Operations
    exprToTerm,
  )
where

import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict (Map)
import Data.Set (Set)
import Data.Text (Text)
import YCHR.Internal.Loc (Ann)
import YCHR.Internal.Parsed (AnnP)
import YCHR.Internal.Types
  ( BoundSig,
    HeadArg,
    Name (..),
    QualifiedConstraint,
    QualifiedName,
    Term (..),
    TypeDefinition,
    TypeExpr,
    flattenName,
    headArgToTerm,
    qualifiedToName,
  )

-- | A resolved program: all modules flattened, equations grouped under
-- their function declarations.
data Program = Program
  { rules :: [Rule],
    functions :: [FunctionDef],
    constraintTypes :: Map QualifiedName [TypeExpr],
    -- | Bounds declared on each @:- chr_constraint@ that carries a
    -- @requiring@ clause. Constraints without bounds do not appear in
    -- this map (rather than mapping to @[]@) so a single membership
    -- check distinguishes "bounded constraint" from "unbounded".
    constraintBounds :: Map QualifiedName [BoundSig],
    functionNames :: Set QualifiedName,
    typeDefinitions :: [TypeDefinition]
  }
  deriving (Show)

-- | A rule in the resolved AST. Structurally identical to the parsed
-- rule; the three head kinds are preserved for desugaring to flatten.
--
-- Guards and bodies are 'Expr' (not 'Term'): the resolver has already
-- decided, for every compound, whether it is a function call, a data
-- constructor application, a dynamic dispatch, a function reference,
-- or a lambda. Downstream passes dispatch structurally and never need
-- to re-check the function-name set.
data Rule = Rule
  { name :: Maybe (Ann Text),
    head :: AnnP Head,
    guard :: AnnP [Expr],
    body :: AnnP [Expr]
  }
  deriving (Show)

-- | Resolved rule head. Mirrors 'YCHR.Internal.Parsed.Head' but with
-- 'QualifiedConstraint' so the constraint-name qualification invariant
-- is reflected in the type. Desugaring flattens the three kinds into
-- the uniform @kept \/ removed@ shape of 'YCHR.Internal.Desugared.Head'.
data Head
  = Simplification [QualifiedConstraint]
  | Propagation [QualifiedConstraint]
  | Simpagation [QualifiedConstraint] [QualifiedConstraint]
  deriving (Show, Eq)

-- | A function definition with its equations grouped together.
data FunctionDef = FunctionDef
  { name :: QualifiedName,
    arity :: Int,
    signatures :: [([TypeExpr], TypeExpr)],
    isOpen :: Bool,
    -- | Bounds declared on this function via @requiring@. Empty when
    -- the function is unbounded.
    requiring :: [BoundSig],
    equations :: [AnnP FunctionEquation]
  }
  deriving (Show)

-- | A function equation. Unlike 'YCHR.Internal.Parsed.FunctionEquation', there
-- is no @funName@ field — the name comes from the enclosing 'FunctionDef'.
--
-- 'args' stays as @[Term]@ because equation arguments are patterns:
-- they are normalized to 'HeadArg's by HNF in the desugarer, and the
-- call-vs-constructor question does not arise for them. 'guard' and
-- 'rhs' carry expression-position 'Expr's.
data FunctionEquation = FunctionEquation
  { args :: [Term],
    guard :: AnnP [Expr],
    rhs :: AnnP (NonEmpty Expr)
  }
  deriving (Show)

-- | An expression in a body, guard, or function RHS. Each constructor
-- corresponds to a single dynamic behavior at runtime, eliminating the
-- ambiguity of the uniform 'Term' shape:
--
--   * 'CallExpr' is a statically-known call to a user-declared
--     function.
--   * 'CtorExpr' is a data constructor application — compiled to a
--     @MakeTerm@ in the VM.
--   * 'ApplyExpr' is dynamic dispatch (the surface @'$call'(F, A1..An)@).
--   * 'FunRefExpr' is a first-class function reference
--     (the surface @fun name/arity@).
--   * 'LambdaExpr' is an anonymous function value. It exists between
--     resolution and the desugarer's lambda-lifting pass, after which
--     it is rewritten to a @__closure@-headed 'CtorExpr'. The
--     parameter list is 'NonEmpty': the resolver rejects
--     @fun() -> Body end@ with 'EmptyLambdaParams' (YCHR-16018) and
--     downstream stages can rely on at least one parameter.
--   * 'HostExpr' is a call into the host language
--     (the surface @host:f(args)@).
data Expr
  = VarExpr Text
  | IntExpr Integer
  | FloatExpr Double
  | TextExpr Text
  | WildcardExpr
  | CtorExpr Name [Expr]
  | CallExpr QualifiedName [Expr]
  | ApplyExpr Expr [Expr]
  | FunRefExpr QualifiedName Int
  | LambdaExpr (NonEmpty HeadArg) (NonEmpty Expr)
  | HostExpr Text [Expr]
  deriving (Show, Eq)

-- | Convert an 'Expr' back to a surface-shaped 'Term'. Used as a
-- narrow bridge for code that still operates on 'Term' (notably the
-- @args@ field of 'YCHR.Internal.Types.QualifiedConstraint', which body-
-- constraint goals carry verbatim).
--
-- The conversion flattens every node to its surface compound shape:
-- 'CallExpr' becomes a 'CompoundTerm' with the function's qualified
-- 'Name' as the head, 'ApplyExpr' becomes a @'$call'@ compound,
-- 'LambdaExpr' becomes its surface @fun(...) -> body@ shape, and so
-- on. The result discards the call/host/apply distinctions — those
-- live only in the 'Expr' tree. This is intentional: every consumer
-- of the resulting 'Term' (e.g. 'YCHR.Internal.Compile.compileTerm',
-- 'YCHR.Run.termToValue') treats every compound as data, which
-- matches CHR's value semantics for constraint arguments and quoted
-- @quote\/1@ subtrees.
exprToTerm :: Expr -> Term
exprToTerm (VarExpr v) = VarTerm v
exprToTerm (IntExpr n) = IntTerm n
exprToTerm (FloatExpr n) = FloatTerm n
exprToTerm (TextExpr s) = TextTerm s
exprToTerm WildcardExpr = Wildcard
exprToTerm (CtorExpr name args) = CompoundTerm name (map exprToTerm args)
exprToTerm (CallExpr qn args) =
  CompoundTerm (qualifiedToName qn) (map exprToTerm args)
exprToTerm (ApplyExpr f args) =
  CompoundTerm (Unqualified "$call") (exprToTerm f : map exprToTerm args)
exprToTerm (HostExpr f args) =
  CompoundTerm (Qualified "host" f) (map exprToTerm args)
exprToTerm (FunRefExpr qn arity) =
  CompoundTerm
    (Unqualified "/")
    [ CompoundTerm (Unqualified (flattenName (qualifiedToName qn))) [],
      IntTerm (fromIntegral arity)
    ]
exprToTerm (LambdaExpr params body) =
  CompoundTerm
    (Unqualified "->")
    [ CompoundTerm (Unqualified "fun") (map headArgToTerm (NE.toList params)),
      sequenceToTerm body
    ]

-- | Re-build a comma-sequence 'Term' from a non-empty list of 'Expr's.
-- Inverse of the comma flattening done at the equation- and lambda-body
-- boundaries.
sequenceToTerm :: NonEmpty Expr -> Term
sequenceToTerm = go . NE.toList
  where
    go [e] = exprToTerm e
    go (e : es) = CompoundTerm (Unqualified ",") [exprToTerm e, go es]
    go [] = CompoundTerm (Unqualified "true") [] -- unreachable: NonEmpty