Skip to content

Ergonomic TTG passes

Motivation

On reddit, @trac-isovector expressed the valid concern that TTG copes quite badly with "minimal" extensions (i.e. concerning only a single extension of HsPar).

We should offer functions that make that use case less boiler-platey, in particular

  1. Creating your own pass should not need 100 type family instance declarations when you only add 1 field compared to i.e. GhcTc
  2. Converting an AST from parameterised by one phase to an AST parameterised by another should ideally only need to specify the cases where something actually happens. We want "batteries included" experience for the nasty "congruence rules" (transform (HsApp ex f a) = HsApp ex (transform f) (transform a)).

Proposal

I don't have a solution yet for 1. (maybe some signature module parameterised on a pass that can be imported for orphan family instances? But then we can't choose to redefine just a fraction of all extension points...).

But for 2. we can just have (only for lambda calculus for illustrative purposes)

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE EmptyDataDecls #-}

module Lib where

type family XApp p :: *
type family XLam p :: *
type family XVar p :: *

type Id = String
data Expr p
  = App (XApp p) (Expr p) (Expr p)
  | Lam (XLam p) Id (Expr p)
  | Var (XVar p) Id

data Vanilla
type instance XApp Vanilla = ()
type instance XLam Vanilla = ()
type instance XVar Vanilla = ()

class Transformer t p1 p2 where
  app :: (Expr p1 -> Expr p2) -> XApp p1 -> Expr p1 -> Expr p1 -> Expr p2
  default app :: (XApp p1 ~ XApp p2) => (Expr p1 -> Expr p2) -> XApp p1 -> Expr p1 -> Expr p1 -> Expr p2
  app t x f a = App x (t f) (t a)
  lam :: (Expr p1 -> Expr p2) -> XLam p1 -> Id -> Expr p1 -> Expr p2
  default lam :: (XLam p1 ~ XLam p2) => (Expr p1 -> Expr p2) -> XLam p1 -> Id -> Expr p1 -> Expr p2
  lam t x id e = Lam x id (t e)
  var :: (Expr p1 -> Expr p2) -> XVar p1 -> Id -> Expr p2
  default var :: (XVar p1 ~ XVar p2) => (Expr p1 -> Expr p2) -> XVar p1 -> Id -> Expr p2
  var _ x id = Var x id

transform :: forall t p1 p2. Transformer t p1 p2 => Expr p1 -> Expr p2
transform (App x f a) = app @t (transform @t) x f a
transform (Lam x id e) = lam @t (transform @t) x id e
transform (Var x id) = var @t @p1 (transform @t) x id

data MyPass

type instance XApp MyPass = ()
type instance XLam MyPass = ()
type instance XVar MyPass = Int

-- case in point: only need to define var to get annotate
data VarLength
instance Transformer VarLength Vanilla MyPass where
  var _ _ id = Var (length id) id

annotate :: Expr Vanilla -> Expr MyPass
annotate = transform @VarLength

This will even properly specialise for the particular Transformer instance, so annotate is as good as hand-written.

This approach would scale to arbitrarily many extension points.

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information