packages feed

fir-0.1.0.0: src/FIR/Syntax/Labels.hs

{-# LANGUAGE AllowAmbiguousTypes    #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs                  #-}
{-# LANGUAGE PolyKinds              #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TypeApplications       #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE UndecidableInstances   #-}

{-|
Module: FIR.Syntax.Labels
Description: @---@ __Optional__ /imperative-style/ syntax using overloaded labels.

Imperative-like syntax for variable assignment using overloaded labels. /Completely optional./

To use this functionality, import this module, activating /OverloadedLabels/ and /RebindableSyntax/.
You'll most likely want to use /TypeApplications/ too.

Usages of label syntax, such as @#foo@, will be desugared
using the 'fromLabel' method of the 'IsLabel' class in this module.

This allows for notation such as:

> do
>   #x #= (1 :: Float)  -- defines a new variable named 'x', initialised with value (1 :: Float)
> 
>   #x .= 2             -- sets the value of variable 'x' to 2
> 
>   #x %= (*3)          -- modifies the value of variable 'x' by multiplying it by 3
> 
>   #y @Float #=! 7     -- defines a new constant named 'y', initialised with value (7 :: Float)
> 
>   z <- #x + #y
>   -- 'z' then has value 6+7 = 13

In the first four examples, the overloaded labels are desugared into standalone labels,
which are then consumed by the infix operators @#=@, @.=@, @%=@ and @#=!@.

In the last example, the labels are desugared directly into values,
within the context of the indexed monad of the @do@ block.

The stateful nature of these labels is worth emphasising.
Consider for instance:

> do
>    #x @Float #= 0
>    x <- #x
>    #x .= 1
>    x' <- #x

When we write @x <- #x@, the /current/ value of @#x@ is obtained from the state.
In this example, this means that @x@ will continue to refer to the value @0@,
even after we set @#x@ to 1. When we retrieve the value of @#x@ after modifying it,
we then give @x'@ the (immutable) value of @1@.

This subtlety is especially important when working with @while@ loops,
as one wants to avoid having a constant conditional in the header.

Note that overloaded labels don't support visible type application in GHC 9.0 [due to a bug](https://gitlab.haskell.org/ghc/ghc/-/issues/19154).
For that version of GHC, you will need to write

> #label #= ( val :: type )

instead of

> #label @type #= val
-}

module FIR.Syntax.Labels
 ( -- * Overloaded labels
   IsLabel(fromLabel)
 , Label(..), LabelUsage(..)
   -- * Infix operators
 , (#=), (#=!), (.=), (%=)
 )
 where

-- base
import Data.Kind
  ( Type )
import qualified GHC.Stack
  ( HasCallStack )
import GHC.TypeLits
  ( Symbol, KnownSymbol )

-- fir
import Control.Type.Optic
  ( Optic, Name )
import FIR.AST
  ( Code )
import FIR.Binding
  ( Var, R, RW )
import FIR.Module
  ( Program )
import FIR.Syntax.Program
  ( def, use, assign, modifying )
import FIR.Prim.Types
  ( PrimTy )
import FIR.ProgramState
  ( ProgramState )
import FIR.Validation.Bindings
  ( ValidDef, AddBinding, Has, CanGet, CanPut )

--------------------------------------------------------------------------
-- overloaded labels

data Label (k :: Symbol) (a :: Type) = Label

data LabelUsage
  = Symbolic
  | Use Symbol ProgramState

class IsLabel k a (usage :: LabelUsage) v
    | v -> a, usage v -> k, v k -> usage
    where
  fromLabel :: GHC.Stack.HasCallStack => v

-- | Use a label symbolically.
--
-- That is, @#foo@ stands for a standalone label.
instance ( KnownSymbol k
         , PrimTy a
         , usage ~ Symbolic
         )
       => IsLabel k a usage (Label k a) where
  fromLabel = Label @k @a

-- | Use a label as a monadic value.
--
-- That is, @#foo@ stands for the value bound at @"foo"@
-- in the context of an indexed state monad.
instance ( KnownSymbol k
         , PrimTy a
         , a ~ Has k i
         , CanGet k i
         , r ~ Code a
         , j ~ i
         , usage ~ 'Use k i
         )
      => IsLabel k a usage (Program i j r) where
  fromLabel = use @(Name k :: Optic '[] i a)

--------------------------------------------------------------------------
-- infix operators

infixr 1 #=
infixr 1 #=!
infixr 1 .=

-- | Define a new variable using a label.
(#=) :: forall a k i.
        ( GHC.Stack.HasCallStack
        , KnownSymbol k
        , ValidDef k i a
        , PrimTy a
        )
     => Label k a
     -> Code a
     -> Program i (AddBinding k (Var RW a) i) (Code a)
_ #= a = def @k @RW a

-- | Define a new constant using a label.
(#=!) :: forall a k i.
        ( GHC.Stack.HasCallStack
        , KnownSymbol k
        , ValidDef k i a
        , PrimTy a
        )
     => Label k a
     -> Code a
     -> Program i (AddBinding k (Var R a) i) (Code a)
_ #=! a = def @k @R a

-- | Set the value of a variable with given label.
(.=) :: forall a k i.
        ( GHC.Stack.HasCallStack
        , KnownSymbol k
        , a ~ Has k i
        , CanPut k i
        , PrimTy a
        )
     => Label k a
     -> Code a
     -> Program i i (Code ())
_ .= a = assign @(Name k :: Optic '[] i a) a

-- | Modify a variable with given label using a function.
(%=) :: forall a k i.
        ( GHC.Stack.HasCallStack
        , KnownSymbol k
        , a ~ Has k i
        , CanGet k i
        , CanPut k i
        )
     => Label k a
     -> (Code a -> Code a)
     -> Program i i (Code ())
_ %= f = modifying @(Name k :: Optic '[] i a) f