swarm-0.4: src/Swarm/Game/World/Compile.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Compiling abstracted combinator expressions ('BTerm') to native
-- Haskell terms. This can supposedly be more efficient than directly
-- interpreting 'BTerm's, but some benchmarking is probably needed to
-- decide whether we want this or not.
--
-- For more info, see:
--
-- https://byorgey.wordpress.com/2023/07/13/compiling-to-intrinsically-typed-combinators/
module Swarm.Game.World.Compile where
import Data.ByteString (ByteString)
import Data.Hash.Murmur (murmur3)
import Data.Kind (Constraint)
import Data.Tagged (Tagged (unTagged))
import Numeric.Noise.Perlin (noiseValue, perlin)
import Swarm.Game.Location (pattern Location)
import Swarm.Game.World.Abstract (BTerm (..))
import Swarm.Game.World.Coords (Coords (..), coordsToLoc)
import Swarm.Game.World.Gen (Seed)
import Swarm.Game.World.Interpret (interpReflect, interpRot)
import Swarm.Game.World.Syntax (Axis (..), Rot, World)
import Swarm.Game.World.Typecheck (Applicable (..), Const (..), Empty (..), NotFun, Over (..))
import Witch (from)
import Witch.Encoding qualified as Encoding
data CTerm a where
CFun :: (CTerm a -> CTerm b) -> CTerm (a -> b)
CConst :: (NotFun a) => a -> CTerm a
instance Applicable CTerm where
CFun f $$ x = f x
compile :: Seed -> BTerm a -> CTerm a
compile seed (BApp b1 b2) = compile seed b1 $$ compile seed b2
compile seed (BConst c) = compileConst seed c
compileConst :: Seed -> Const a -> CTerm a
compileConst seed = \case
CLit i -> CConst i
CCell c -> CConst c
CFI -> unary fromIntegral
CIf -> CFun $ \(CConst b) -> CFun $ \t -> CFun $ \e -> if b then t else e
CNot -> unary not
CNeg -> unary negate
CAbs -> unary abs
CAnd -> binary (&&)
COr -> binary (||)
CAdd -> binary (+)
CSub -> binary (-)
CMul -> binary (*)
CDiv -> binary (/)
CIDiv -> binary div
CMod -> binary mod
CEq -> binary (==)
CNeq -> binary (/=)
CLt -> binary (<)
CLeq -> binary (<=)
CGt -> binary (>)
CGeq -> binary (>=)
CMask -> compileMask
CSeed -> CConst (fromIntegral seed)
CCoord ax -> CFun $ \(CConst (coordsToLoc -> Location x y)) -> CConst (fromIntegral (case ax of X -> x; Y -> y))
CHash -> compileHash
CPerlin -> compilePerlin
CReflect ax -> compileReflect ax
CRot rot -> compileRot rot
COver -> binary (<!>)
K -> CFun $ \x -> CFun $ const x
S -> CFun $ \f -> CFun $ \g -> CFun $ \x -> f $$ x $$ (g $$ x)
I -> CFun id
B -> CFun $ \f -> CFun $ \g -> CFun $ \x -> f $$ (g $$ x)
C -> CFun $ \f -> CFun $ \x -> CFun $ \y -> f $$ y $$ x
Φ -> CFun $ \c -> CFun $ \f -> CFun $ \g -> CFun $ \x -> c $$ (f $$ x) $$ (g $$ x)
unary :: (NotFun a, NotFun b) => (a -> b) -> CTerm (a -> b)
unary op = CFun $ \(CConst x) -> CConst (op x)
binary :: (NotFun a, NotFun b, NotFun c) => (a -> b -> c) -> CTerm (a -> b -> c)
binary op = CFun $ \(CConst x) -> CFun $ \(CConst y) -> CConst (op x y)
-- Note we could desugar 'mask p a -> if p a empty' but that would
-- require an explicit 'empty' node, whose type can't be inferred.
compileMask :: (NotFun a, Empty a) => CTerm (World Bool -> World a -> World a)
compileMask = CFun $ \p -> CFun $ \a -> CFun $ \ix ->
case p $$ ix of
CConst b -> if b then a $$ ix else CConst empty
compileHash :: CTerm (Coords -> Integer)
compileHash = CFun $ \(CConst (Coords ix)) -> CConst (fromIntegral (h ix))
where
h = murmur3 0 . unTagged . from @String @(Encoding.UTF_8 ByteString) . show
compilePerlin :: CTerm (Integer -> Integer -> Double -> Double -> World Double)
compilePerlin =
CFun $ \(CConst s) ->
CFun $ \(CConst o) ->
CFun $ \(CConst k) ->
CFun $ \(CConst p) ->
let noise = perlin (fromIntegral s) (fromIntegral o) k p
in CFun $ \(CConst (Coords ix)) -> CConst (sample ix noise)
where
sample (i, j) noise = noiseValue noise (fromIntegral i / 2, fromIntegral j / 2, 0)
compileReflect :: Axis -> CTerm (World a -> World a)
compileReflect ax = CFun $ \w -> CFun $ \(CConst c) -> w $$ CConst (interpReflect ax c)
compileRot :: Rot -> CTerm (World a -> World a)
compileRot rot = CFun $ \w -> CFun $ \(CConst c) -> w $$ CConst (interpRot rot c)
type family NoFunParams a :: Constraint where
NoFunParams (a -> b) = (NotFun a, NoFunParams b)
NoFunParams _ = ()
-- | Interpret a compiled term into the host language.
runCTerm :: (NoFunParams a) => CTerm a -> a
runCTerm (CConst a) = a
runCTerm (CFun f) = runCTerm . f . CConst