packages feed

free-foil-0.4.0: bench/alphaequiv/Main.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE DeriveTraversable   #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell     #-}

-- | What the rename branches of 'alphaEquivScoped' cost.
--
-- The zipmatchk benchmark compares a term against itself, so the binders
-- always coincide and 'unifyPatternsIn' answers 'SameNameBinders': the
-- fast path, no renaming. This benchmark forces the other branches: @t2@
-- is α-equivalent to @t1@ but every binder carries a different raw name
-- (it is built under a dummy binder and then 'unsinkAST'-ed back to the
-- empty scope), so every level takes a Rename* branch. An eager
-- implementation materialised a renamed copy of the whole remaining body
-- per level, which is quadratic in the depth of the chain; with the
-- renaming threaded down the recursion, all three columns must stay
-- linear and within a small factor of one another.
module Main (main) where

import           Data.Bifunctor.TH
import           Data.Maybe              (fromMaybe)
import           Test.Tasty.Bench

import qualified Control.Monad.Foil      as Foil
import           Control.Monad.Free.Foil
import           Data.ZipMatchK.TH       (deriveZipMatchK)

data LamSig scope term
  = App term term
  | Lam scope
  deriving (Functor, Foldable, Traversable)

deriveBifunctor ''LamSig
deriveBifoldable ''LamSig
deriveBitraversable ''LamSig
deriveZipMatchK ''LamSig

type Term = AST Foil.NameBinder LamSig

-- | λx1. λx2. … λxn. xn, allocated in the given scope.
chainIn :: Foil.Distinct n => Foil.Scope n -> Int -> Foil.Name n -> Term n
chainIn _scope 0 x = Var x
chainIn scope k _x = Foil.withFresh scope $ \binder ->
  let scope' = Foil.extendScope binder scope
   in Node (Lam (ScopedAST binder (chainIn scope' (k - 1) (Foil.nameOf binder))))

-- | The chain over the empty scope: binders 0, 1, …, n−1.
plain :: Int -> Term Foil.VoidS
plain n = Foil.withFresh Foil.emptyScope $ \b0 ->
  let scope0 = Foil.extendScope b0 Foil.emptyScope
   in Node (Lam (ScopedAST b0 (chainIn scope0 (n - 1) (Foil.nameOf b0))))

-- | The same chain built under one dummy binder and cut back down:
-- binders 1, 2, …, n, so every level differs from 'plain' by exactly one.
shifted :: Int -> Term Foil.VoidS
shifted n = Foil.withFresh Foil.emptyScope $ \dummy ->
  let scope1 = Foil.extendScope dummy Foil.emptyScope
      t = Foil.withFresh scope1 $ \b1 ->
            let scope2 = Foil.extendScope b1 scope1
             in Node (Lam (ScopedAST b1 (chainIn scope2 (n - 1) (Foil.nameOf b1))))
   in fromMaybe (error "the chain uses the dummy binder")
        (unsinkAST Foil.emptyScope t)

sized :: Int -> Benchmark
sized n =
  let t1 = plain n
      t2 = shifted n
   in if not (alphaEquiv Foil.emptyScope t1 t2)
        then error "the two chains are not alpha-equivalent"
        else bgroup (show n <> " nested binders")
               [ bench "same binders (fast path)" $
                   whnf (alphaEquiv Foil.emptyScope t1) t1
               , bench "all binders differ (rename path)" $
                   whnf (alphaEquiv Foil.emptyScope t1) t2
               , bench "all binders differ, alphaEquivRefreshed" $
                   whnf (alphaEquivRefreshed Foil.emptyScope t1) t2
               ]

main :: IO ()
main = defaultMain [ sized n | n <- [250, 500, 1000, 2000] ]