packages feed

canontra-0.3.0.0: src/Canontra/Normalize/Algebraic.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Canontra.Normalize.Algebraic
Description : Pure commutative, associative, and relational algebraic canonicalizer.

Canonicalizes provably pure, side-effect-free expressions:
  - Commutative & associative binary operators (+, *, &, |, ^, ==, !=)
  - Relational dual operators (b > a -> a < b, b >= a -> a <= b)
  - Unordered collection literals (ExprSet and pure ExprDict)
-}
module Canontra.Normalize.Algebraic
  ( normalizeAlgebraicProgram
  , normalizeAlgebraicModule
  , normalizeAlgebraicDecl
  , normalizeAlgebraicStmt
  , normalizeAlgebraicExpr
  , normalizeBinaryExpr
  , normalizeCollectionLiteral
  , isProvablyPureExpr
  , isCommutativeOp
  , hashExprB
  ) where

import Data.Bits (xor)
import qualified Data.ByteString as BS
import Data.List (sortBy)
import Data.Ord (comparing)
import Data.Word (Word64)

import Canontra.Canonical.Serialize (canonicalizeExpr)
import Canontra.IR.Declaration
  ( Class (..)
  , Declaration (..)
  , Function (..)
  , Impl (..)
  , Struct (..)
  , Trait (..)
  )
import Canontra.IR.Expression
  ( CompFor (..)
  , Expr (..)
  , FStringPart (..)
  , Lit (..)
  , MatchCase (..)
  , Op (..)
  , SelectCase (..)
  , Stmt (..)
  )
import Canontra.IR.Program
  ( Module (..)
  , Program (..)
  )

-- | Compute a deterministic 64-bit FNV-1a hash of an 'Expr' for canonical ordering.
hashExprB :: Expr -> Word64
hashExprB = fnv1aHash64 . canonicalizeExpr
  where
    fnv1aHash64 :: BS.ByteString -> Word64
    fnv1aHash64 =
      BS.foldl'
        (\h b -> (h `xor` fromIntegral b) * 1099511628211)
        14695981039346656037

-- | Check whether an expression is provably pure (free of function calls,
-- method invocations, walrus assignments, await, yield, or macro invocations).
isProvablyPureExpr :: Expr -> Bool
isProvablyPureExpr = \case
  ExprLit _             -> True
  ExprId _              -> True
  ExprBinary _ e1 e2    -> isProvablyPureExpr e1 && isProvablyPureExpr e2
  ExprUnary _ e         -> isProvablyPureExpr e
  ExprTuple es          -> all isProvablyPureExpr es
  ExprList es           -> all isProvablyPureExpr es
  ExprSet es            -> all isProvablyPureExpr es
  ExprDict pairs        -> all (\(k, v) -> isProvablyPureExpr k && isProvablyPureExpr v) pairs
  ExprAttr e _          -> isProvablyPureExpr e
  ExprOptChain e _      -> isProvablyPureExpr e
  ExprNullish e1 e2     -> isProvablyPureExpr e1 && isProvablyPureExpr e2
  ExprSubscript e1 e2   -> isProvablyPureExpr e1 && isProvablyPureExpr e2
  ExprTernary c t f     -> isProvablyPureExpr c && isProvablyPureExpr t && isProvablyPureExpr f
  _                     -> False

-- | Check whether an expression is a string, bytes, or sequence literal for
-- which '+' represents non-commutative concatenation rather than addition.
isConcatenationOperand :: Expr -> Bool
isConcatenationOperand = \case
  ExprLit (LitString _)   -> True
  ExprLit (LitBytes _)    -> True
  ExprFormattedString _   -> True
  ExprList _              -> True
  ExprTuple _             -> True
  ExprBinary OpAdd a b    -> isConcatenationOperand a || isConcatenationOperand b
  _                       -> False

-- | True for commutative binary operators.
isCommutativeOp :: Op -> Bool
isCommutativeOp = \case
  OpAdd    -> True
  OpMul    -> True
  OpBitAnd -> True
  OpBitOr  -> True
  OpBitXor -> True
  OpEq     -> True
  OpNotEq  -> True
  _        -> False

-- | True for operators that are both associative and commutative on pure expressions.
isAssociativeCommutativeOp :: Op -> Bool
isAssociativeCommutativeOp = \case
  OpAdd    -> True
  OpMul    -> True
  OpBitAnd -> True
  OpBitOr  -> True
  OpBitXor -> True
  _        -> False

-- | Apply algebraic normalization across an entire 'Program'.
normalizeAlgebraicProgram :: Program -> Program
normalizeAlgebraicProgram (Program modules lang) =
  Program (map normalizeAlgebraicModule modules) lang

-- | Apply algebraic normalization across a 'Module'.
normalizeAlgebraicModule :: Module -> Module
normalizeAlgebraicModule (Module name imps decls stmts) =
  Module
    name
    imps
    (map normalizeAlgebraicDecl decls)
    (map normalizeAlgebraicStmt stmts)

-- | Apply algebraic normalization to a 'Declaration'.
normalizeAlgebraicDecl :: Declaration -> Declaration
normalizeAlgebraicDecl = \case
  DeclFunction fn ->
    DeclFunction (normalizeFunction fn)
  DeclClass cls ->
    DeclClass cls { clsMethods = map normalizeFunction (clsMethods cls) }
  DeclStruct st ->
    DeclStruct st { stMethods = map normalizeFunction (stMethods st) }
  DeclTrait tr ->
    DeclTrait tr { trMethods = map normalizeFunction (trMethods tr) }
  DeclImpl imp ->
    DeclImpl imp { impMethods = map normalizeFunction (impMethods imp) }
  DeclReceiver rc fn ->
    DeclReceiver rc (normalizeFunction fn)
  other -> other
  where
    normalizeFunction fn =
      fn { fnBody = map normalizeAlgebraicStmt (fnBody fn) }

-- | Apply algebraic normalization to a 'Stmt'.
normalizeAlgebraicStmt :: Stmt -> Stmt
normalizeAlgebraicStmt = \case
  StmtAssign targets val ->
    StmtAssign (map normalizeAlgebraicExpr targets) (normalizeAlgebraicExpr val)
  StmtAugAssign target op val ->
    StmtAugAssign (normalizeAlgebraicExpr target) op (normalizeAlgebraicExpr val)
  StmtAnnAssign target ann mVal ->
    StmtAnnAssign (normalizeAlgebraicExpr target) ann (fmap normalizeAlgebraicExpr mVal)
  StmtReturn mExpr ->
    StmtReturn (fmap normalizeAlgebraicExpr mExpr)
  StmtExpr expr ->
    StmtExpr (normalizeAlgebraicExpr expr)
  StmtIf cond thenB elseB ->
    StmtIf
      (normalizeAlgebraicExpr cond)
      (map normalizeAlgebraicStmt thenB)
      (map normalizeAlgebraicStmt elseB)
  StmtWhile cond body elseB ->
    StmtWhile
      (normalizeAlgebraicExpr cond)
      (map normalizeAlgebraicStmt body)
      (map normalizeAlgebraicStmt elseB)
  StmtFor target iter body elseB ->
    StmtFor
      (normalizeAlgebraicExpr target)
      (normalizeAlgebraicExpr iter)
      (map normalizeAlgebraicStmt body)
      (map normalizeAlgebraicStmt elseB)
  StmtAsyncFor target iter body elseB ->
    StmtAsyncFor
      (normalizeAlgebraicExpr target)
      (normalizeAlgebraicExpr iter)
      (map normalizeAlgebraicStmt body)
      (map normalizeAlgebraicStmt elseB)
  StmtWith items body ->
    StmtWith
      [ (normalizeAlgebraicExpr ctx, fmap normalizeAlgebraicExpr mVar)
      | (ctx, mVar) <- items
      ]
      (map normalizeAlgebraicStmt body)
  StmtAsyncWith items body ->
    StmtAsyncWith
      [ (normalizeAlgebraicExpr ctx, fmap normalizeAlgebraicExpr mVar)
      | (ctx, mVar) <- items
      ]
      (map normalizeAlgebraicStmt body)
  StmtTry tryB handlers elseB finB ->
    StmtTry
      (map normalizeAlgebraicStmt tryB)
      [ (fmap normalizeAlgebraicExpr mExc, mAlias, map normalizeAlgebraicStmt hBody)
      | (mExc, mAlias, hBody) <- handlers
      ]
      (map normalizeAlgebraicStmt elseB)
      (map normalizeAlgebraicStmt finB)
  StmtRaise mExc mCause ->
    StmtRaise (fmap normalizeAlgebraicExpr mExc) (fmap normalizeAlgebraicExpr mCause)
  StmtAssert test mMsg ->
    StmtAssert (normalizeAlgebraicExpr test) (fmap normalizeAlgebraicExpr mMsg)
  StmtDelete targets ->
    StmtDelete (map normalizeAlgebraicExpr targets)
  StmtMatch subject cases ->
    StmtMatch
      (normalizeAlgebraicExpr subject)
      [ MatchCase
          (normalizeAlgebraicExpr pat)
          (fmap normalizeAlgebraicExpr mGuard)
          (map normalizeAlgebraicStmt body)
      | MatchCase pat mGuard body <- cases
      ]
  StmtSwitch subject cases defStmts ->
    StmtSwitch
      (normalizeAlgebraicExpr subject)
      [ (normalizeAlgebraicExpr cExpr, map normalizeAlgebraicStmt cStmts)
      | (cExpr, cStmts) <- cases
      ]
      (map normalizeAlgebraicStmt defStmts)
  StmtSelect clauses ->
    StmtSelect
      [ (normalizeComm comm, map normalizeAlgebraicStmt body)
      | (comm, body) <- clauses
      ]
  StmtChanSend ch val ->
    StmtChanSend (normalizeAlgebraicExpr ch) (normalizeAlgebraicExpr val)
  StmtGo expr ->
    StmtGo (normalizeAlgebraicExpr expr)
  StmtDefer expr ->
    StmtDefer (normalizeAlgebraicExpr expr)
  StmtLoop body ->
    StmtLoop (map normalizeAlgebraicStmt body)
  other -> other
  where
    normalizeComm = \case
      SelectSend ch val -> SelectSend (normalizeAlgebraicExpr ch) (normalizeAlgebraicExpr val)
      SelectRecv mV ch  -> SelectRecv mV (normalizeAlgebraicExpr ch)
      SelectDefault     -> SelectDefault

-- | Recursively canonicalize an 'Expr' bottom-up using algebraic identities.
normalizeAlgebraicExpr :: Expr -> Expr
normalizeAlgebraicExpr = \case
  ExprLit lit -> ExprLit lit
  ExprId ident -> ExprId ident
  ExprBinary op lhs rhs ->
    let lhs' = normalizeAlgebraicExpr lhs
        rhs' = normalizeAlgebraicExpr rhs
    in normalizeBinaryExpr op lhs' rhs'
  ExprUnary op operand ->
    let op' = normalizeAlgebraicExpr operand
    in case (op, op') of
         (OpNot, ExprUnary OpNot inner) -> inner
         _                              -> ExprUnary op op'
  ExprCall callee args kwArgs ->
    ExprCall
      (normalizeAlgebraicExpr callee)
      (map normalizeAlgebraicExpr args)
      [(k, normalizeAlgebraicExpr v) | (k, v) <- kwArgs]
  ExprLambda params body ->
    ExprLambda params (normalizeAlgebraicExpr body)
  ExprTernary cond thenE elseE ->
    ExprTernary
      (normalizeAlgebraicExpr cond)
      (normalizeAlgebraicExpr thenE)
      (normalizeAlgebraicExpr elseE)
  ExprList items ->
    ExprList (map normalizeAlgebraicExpr items)
  ExprTuple items ->
    ExprTuple (map normalizeAlgebraicExpr items)
  ExprSet items ->
    normalizeCollectionLiteral (ExprSet (map normalizeAlgebraicExpr items))
  ExprDict pairs ->
    normalizeCollectionLiteral
      (ExprDict [(normalizeAlgebraicExpr k, normalizeAlgebraicExpr v) | (k, v) <- pairs])
  ExprListComp elt comps ->
    ExprListComp (normalizeAlgebraicExpr elt) (map normalizeComp comps)
  ExprSetComp elt comps ->
    ExprSetComp (normalizeAlgebraicExpr elt) (map normalizeComp comps)
  ExprGenerator elt comps ->
    ExprGenerator (normalizeAlgebraicExpr elt) (map normalizeComp comps)
  ExprDictComp kExpr vExpr comps ->
    ExprDictComp
      (normalizeAlgebraicExpr kExpr)
      (normalizeAlgebraicExpr vExpr)
      (map normalizeComp comps)
  ExprAttr obj attr ->
    ExprAttr (normalizeAlgebraicExpr obj) attr
  ExprOptChain obj attr ->
    ExprOptChain (normalizeAlgebraicExpr obj) attr
  ExprNullish lhs rhs ->
    ExprNullish (normalizeAlgebraicExpr lhs) (normalizeAlgebraicExpr rhs)
  ExprSubscript obj idx ->
    ExprSubscript (normalizeAlgebraicExpr obj) (normalizeAlgebraicExpr idx)
  ExprSlice mLower mUpper mStep ->
    ExprSlice
      (fmap normalizeAlgebraicExpr mLower)
      (fmap normalizeAlgebraicExpr mUpper)
      (fmap normalizeAlgebraicExpr mStep)
  ExprAwait operand ->
    ExprAwait (normalizeAlgebraicExpr operand)
  ExprYield mOperand ->
    ExprYield (fmap normalizeAlgebraicExpr mOperand)
  ExprYieldFrom operand ->
    ExprYieldFrom (normalizeAlgebraicExpr operand)
  ExprStarred operand ->
    ExprStarred (normalizeAlgebraicExpr operand)
  ExprKwStarred operand ->
    ExprKwStarred (normalizeAlgebraicExpr operand)
  ExprWalrus name val ->
    ExprWalrus name (normalizeAlgebraicExpr val)
  ExprFormattedString parts ->
    ExprFormattedString (map normalizeFPart parts)
  ExprChanRecv ch ->
    ExprChanRecv (normalizeAlgebraicExpr ch)
  ExprTryOp operand ->
    ExprTryOp (normalizeAlgebraicExpr operand)
  ExprJSX tag attrs children ->
    ExprJSX
      tag
      [(k, normalizeAlgebraicExpr v) | (k, v) <- attrs]
      (map normalizeAlgebraicExpr children)
  ExprMacroCall name args ->
    ExprMacroCall name (map normalizeAlgebraicExpr args)
  where
    normalizeComp (CompFor tgt iter ifs) =
      CompFor
        (normalizeAlgebraicExpr tgt)
        (normalizeAlgebraicExpr iter)
        (map normalizeAlgebraicExpr ifs)
    normalizeFPart = \case
      FStringText txt -> FStringText txt
      FStringExpr e mC mS -> FStringExpr (normalizeAlgebraicExpr e) mC mS

-- | Normalize a binary expression (assuming children are already normalized):
--   1. Relational duals: @b > a -> a < b@, @b >= a -> a <= b@ (when pure)
--   2. Associative + commutative flattening and sorting for @+, *, &, |, ^@ (when pure)
--   3. Commutative sorting for @==, !=@ (when pure)
normalizeBinaryExpr :: Op -> Expr -> Expr -> Expr
normalizeBinaryExpr op e1 e2 = case op of
  OpGt
    | isProvablyPureExpr e1 && isProvablyPureExpr e2 ->
        ExprBinary OpLt e2 e1
    | otherwise ->
        ExprBinary OpGt e1 e2

  OpGtE
    | isProvablyPureExpr e1 && isProvablyPureExpr e2 ->
        ExprBinary OpLtE e2 e1
    | otherwise ->
        ExprBinary OpGtE e1 e2

  _
    | isAssociativeCommutativeOp op && canCommute op e1 e2 ->
        let terms = flattenAssocOp op e1 ++ flattenAssocOp op e2
            sortedTerms = sortBy (comparing hashExprB) terms
        in case sortedTerms of
             (t:ts) -> foldl (ExprBinary op) t ts
             []     -> ExprBinary op e1 e2
    | isCommutativeOp op && canCommute op e1 e2 ->
        if hashExprB e1 <= hashExprB e2
          then ExprBinary op e1 e2
          else ExprBinary op e2 e1
    | otherwise ->
        ExprBinary op e1 e2
  where
    canCommute OpAdd a b =
      isProvablyPureExpr a
      && isProvablyPureExpr b
      && not (isConcatenationOperand a)
      && not (isConcatenationOperand b)
    canCommute _ a b =
      isProvablyPureExpr a && isProvablyPureExpr b

    flattenAssocOp targetOp expr = case expr of
      ExprBinary innerOp a b
        | innerOp == targetOp && canCommute targetOp a b ->
            flattenAssocOp targetOp a ++ flattenAssocOp targetOp b
      _ -> [expr]

-- | Normalize unordered collection literals ('ExprSet' and 'ExprDict') by sorting
-- elements/entries by their canonical FNV-1a structural hash when all elements are pure.
normalizeCollectionLiteral :: Expr -> Expr
normalizeCollectionLiteral = \case
  ExprSet items
    | all isProvablyPureExpr items ->
        ExprSet (sortBy (comparing hashExprB) items)
    | otherwise ->
        ExprSet items
  ExprDict pairs
    | all (\(k, v) -> isProvablyPureExpr k && isProvablyPureExpr v) pairs ->
        ExprDict (sortBy (comparing (\(k, v) -> (hashExprB k, hashExprB v))) pairs)
    | otherwise ->
        ExprDict pairs
  other -> other