packages feed

moonlight-core-0.1.0.0: src-term/Moonlight/Core/Term/Database/Canonicalize.hs

{-# LANGUAGE ScopedTypeVariables #-}

-- | Canonicalisation of a term 'Database' under a key-renaming, plus entry
-- enumeration and tuple rehydration.
module Moonlight.Core.Term.Database.Canonicalize
  ( canonicalizeDatabase,
    canonicalizeDirtyRows,
    canonicalizeDirtyRowsInEditor,
  )
where

import Control.Monad.ST (ST)
import Data.IntMap.Strict (IntMap)
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Kind (Type)
import Data.Maybe (mapMaybe)
import Moonlight.Core.DenseKey (DenseKey (..))
import Moonlight.Core.Language (Language)
import Moonlight.Core.Term.Database
  ( Database,
    DatabaseEditor,
    DatabaseRow,
    DatabaseRowDelta (..),
    RowId,
    RowIdSet,
    TermCommitResult (..),
    TermCommand (..),
    rowChildren,
    rowResult,
    rowEntry,
    editorQueueCommands,
    editorSnapshot,
    commitTermCommands,
    mapRowKeys,
    normalizeTermCommands,
    runCommandTransaction,
  )
import Moonlight.Core.Term.Database.Projection
  ( dirtyRowsForKeysByOperatorId,
    operatorMapFromShapes,
    operatorRowsByOperatorId,
    operatorRowsForIdsByOperatorId,
  )
import Moonlight.Core.Term.Database.Types (operatorShapes)
import Prelude

-- | Rewrite every key in the database through the supplied renaming in a
-- single pass.
--
-- A congruence-closed renaming returns the rewritten database as 'Right'. Rows
-- that collide after rewriting return the residual congruence unions as
-- 'Left'; the partially rewritten database is deliberately not published.
-- Callers holding an open union-find should discharge those commands and
-- re-invoke with the updated renaming.
canonicalizeDatabase ::
  (DenseKey key, Language f) =>
  (key -> key) ->
  Database f key ->
  Either [TermCommand f key] (Database f key)
canonicalizeDatabase canonicalize db =
  case runCommandTransaction db (canonicalizeDirtyRowsInEditor (allKeys db) canonicalize) of
    (_result, [], canonicalDatabase) ->
      Right canonicalDatabase
    (_result, residualCommands, _partiallyCanonicalDatabase) ->
      Left residualCommands

canonicalizeDirtyRows ::
  (DenseKey key, Language f) =>
  IntSet ->
  (key -> key) ->
  Database f key ->
  (DatabaseRowDelta f, [TermCommand f key], Database f key)
canonicalizeDirtyRows dirtyKeys canonicalize db =
  canonicalizationResult plan (commitTermCommands (planCommands plan) db)
  where
    plan =
      canonicalizationPlan dirtyKeys canonicalize db

canonicalizeDirtyRowsInEditor ::
  (DenseKey key, Language f) =>
  IntSet ->
  (key -> key) ->
  DatabaseEditor s f key ->
  ST s (DatabaseRowDelta f, [TermCommand f key])
canonicalizeDirtyRowsInEditor dirtyKeys canonicalize editor = do
  snapshot <- editorSnapshot editor
  let plan =
        canonicalizationPlan dirtyKeys canonicalize snapshot
  commitResult <- editorQueueCommands (planCommands plan) editor
  let (delta, commands, _canonicalDb) =
        canonicalizationResult plan commitResult
  pure (delta, commands)

type CanonicalizationPlan :: (Type -> Type) -> Type -> Type
data CanonicalizationPlan f key = CanonicalizationPlan
  { deletedRowsByOperatorId :: !(IntMap [(RowId, DatabaseRow)]),
    planCommands :: ![TermCommand f key]
  }

canonicalizationPlan ::
  (DenseKey key, Language f) =>
  IntSet ->
  (key -> key) ->
  Database f key ->
  CanonicalizationPlan f key
canonicalizationPlan dirtyKeys canonicalize db =
  CanonicalizationPlan
    { deletedRowsByOperatorId = dirtyRows,
      planCommands =
        deleteRowCommands db dirtyRows
          <> insertRowCommands db candidateInsertedRows
    }
  where
    dirtyRows =
      rowsSelectedBy (dirtyRowsForKeysByOperatorId db dirtyKeys) db

    candidateInsertedRows =
      fmap (fmap (canonicalizeRow canonicalize . snd)) dirtyRows

canonicalizationResult ::
  (Ord key, Language f) =>
  CanonicalizationPlan f key ->
  TermCommitResult f key ->
  (DatabaseRowDelta f, [TermCommand f key], Database f key)
canonicalizationResult plan commitResult =
  (delta, commands, committedDatabase commitResult)
  where
    delta =
      DatabaseRowDelta
        { rowsDeleted =
            operatorMapFromShapes
              (committedDatabase commitResult)
              (deletedRowsByOperatorId plan),
          rowsInserted = insertedRows commitResult
        }

    commands =
      normalizeTermCommands (planCommands plan <> residualCommands commitResult)

deleteRowCommands ::
  forall f key.
  Database f key ->
  IntMap [(RowId, DatabaseRow)] ->
  [TermCommand f key]
deleteRowCommands database =
  IntMap.foldMapWithKey operatorDeleteCommands
  where
    operatorDeleteCommands ::
      Int ->
      [(RowId, DatabaseRow)] ->
      [TermCommand f key]
    operatorDeleteCommands operatorId rows =
      maybe
        []
        (\operator -> fmap (DeleteRow operator . fst) rows)
        (IntMap.lookup operatorId (operatorShapes database))

insertRowCommands ::
  forall key f.
  (DenseKey key, Language f) =>
  Database f key ->
  IntMap [DatabaseRow] ->
  [TermCommand f key]
insertRowCommands database =
  IntMap.foldMapWithKey operatorInsertCommands
  where
    operatorInsertCommands ::
      Int ->
      [DatabaseRow] ->
      [TermCommand f key]
    operatorInsertCommands operatorId rows =
      maybe
        []
        (\operator -> mapMaybe (fmap (uncurry InsertTerm) . rowEntry operator) rows)
        (IntMap.lookup operatorId (operatorShapes database))

rowsSelectedBy ::
  IntMap RowIdSet ->
  Database f key ->
  IntMap [(RowId, DatabaseRow)]
rowsSelectedBy selectedRows db =
  IntMap.mapWithKey
    (\operatorId rowIds -> operatorRowsForIdsByOperatorId operatorId rowIds db)
    selectedRows

canonicalizeRow :: DenseKey key => (key -> key) -> DatabaseRow -> DatabaseRow
canonicalizeRow canonicalize row =
  mapRowKeys canonicalizeKey row
  where
    canonicalizeKey =
      encodeDenseKey . canonicalize . decodeDenseKey

allKeys :: Database f key -> IntSet
allKeys db =
  IntSet.fromList
    [ key
      | rows <- IntMap.elems (operatorRowsByOperatorId db),
        (_rowId, row) <- rows,
        key <- rowResult row : rowChildren row
    ]