open import 1Lab.Reflection.Signature
open import 1Lab.Reflection.Subst
open import 1Lab.Reflection
open import 1Lab.Path
open import 1Lab.Type
module 1Lab.Reflection.Copattern where
--------------------------------------------------------------------------------
-- Macros for manipulating copattern definitions.
-- Make a top-level copattern binding for an existing record.
make-copattern : Bool → Name → Term → Term → TC ⊤
make-copattern declare? def-name tm tp = do
-- Ensure that codomain is a record type.
let tele , cod-tp = pi-view tp
def rec-name params ← pure cod-tp
where _ → typeError [ "make-copattern: codomain of " , termErr tp , " is not a record type." ]
let inst-tm = apply-tm* tm (tel→args 0 tele)
-- Construct copattern clauses for each field.
ctor , fields ← get-record-type rec-name
clauses ←
in-context (reverse tele) $
for fields λ (arg field-info field-name) → do
-- Infer the type of the field with all known arguments instantiated.
field-tp ← infer-type (def field-name (argN inst-tm ∷ []))
-- Agda will insert implicits when defining copatterns even
-- with 'withExpandLast true', so we need to do implicit instantiation
-- by hand. There are also cases where it's better to fully
-- eta-expand than not (e.g. the 'helper' we're expanding has a
-- field defined by lazy matching, which does not reduce unless
-- applied, and would cause duplication of the big input term). So
-- we fully eta-expand clauses here.
-- First, we strip off all leading quantifiers from the field
-- type.
let
(field-tele , tp) = pi-view field-tp
nargs = length field-tele
clause-tele = tele ++ field-tele
-- Construct the pattern portion of the clause, making sure to
-- bind all variables. Note that copattern projections are always
-- visible.
let
pat = tel→pats nargs tele ++
arg (set-visibility visible field-info) (proj field-name) ∷
tel→pats 0 field-tele
-- Construct the body of the clause, making sure to apply all
-- arguments bound outside the copattern match, and apply the
-- eta-expanded arguments. We also need to apply all of the
-- implicit arguments to 'tm'.
body ←
in-context (reverse clause-tele) $
reduce (def field-name (raise nargs inst-tm v∷ tel→args 0 field-tele))
-- Construct the final clause.
pure $ clause clause-tele pat body
-- Define a copattern binding, and predeclare its type if required.
case declare? of λ where
true → declare (argN def-name) tp <|> pure tt
false → pure tt
-- Construct the final copattern.
define-function def-name clauses
-- Repack a record type.
-- Will also accept functions into record types like `A → Record`,
-- and will perform a pointwise repacking.
repack-record : Term → Term → TC Term
repack-record tm tp = do
-- Ensure that codomain is a record type.
tele , cod-tp ← pi-view <$> reduce tp
def rec-name params ← pure cod-tp
where _ → typeError [ "repack-record: codomain of " , termErr tp , " is not a record type." ]
-- Instantiate the term with all telescope arguments to saturate it.
let inst-tm = apply-tm* tm (tel→args 0 tele)
-- Construct fields for each projection.
ctor , fields ← get-record-type rec-name
args ←
in-context (reverse tele) $
for fields λ (arg field-info field-name) →
argN <$> reduce (def field-name (argN inst-tm ∷ []))
-- Builld a pointwise repacking.
pure (tel→lam tele (con ctor args))
-- Helper for the 'define' macros; Unifies the given goal with the type
-- of the given function, if it has been defined. If the function has
-- not been defined, and the first argument is 'false', then an error is
-- raised.
type-for : String → Bool → Name → Term → TC ⊤
type-for tac decl? fun goal with decl?
... | true = (unify goal =<< get-type fun) <|> pure tt
... | false = (unify goal =<< get-type fun) <|> typeError
[ "define-" , strErr tac , ": the function " , nameErr fun , " should already have been declared."
]
--------------------------------------------------------------------------------
-- Usage
{-
Write the following in a module:
> unquoteDecl copat = declare-copattern copat thing-to-be-expanded
If you wish to give the binding a type annotation, you can also use
> copat : Your-type
> unquoteDecl copat = declare-copattern copat thing-to-be-expanded
Note that, in this case, the thing-to-be-expanded must have exactly the
same type as the binding `copat`. All features of non-recursive records
are supported, including instance fields and fields with implicit
arguments.
These macros also allow you to lift functions 'A → some-record-type'
into copattern definitions. Note that Agda will generate meta for
implicits before performing quotation, so we need to explicitly
bind all implicits using a lambda before quotation!
-}
declare-copattern : ∀ {ℓ} {A : Type ℓ} → Name → A → TC ⊤
declare-copattern {A = A} nm x = do
`x ← quoteTC x
`A ← quoteTC A
make-copattern true nm `x `A
define-copattern
: ∀ {ℓ} (nm : Name)
→ {@(tactic (type-for "copattern" true nm)) A : Type ℓ}
→ A → TC ⊤
define-copattern nm {A = A} x = do
`A ← quoteTC A
`x ← define-abbrev nm "value" `A =<< quoteTC x
make-copattern false nm `x `A
{-
There is a slight caveat with level-polymorphic definitions, as
they cannot be quoted into any `Type ℓ`. With this in mind,
we also provide a pair of macros that work over `Typeω` instead.
-}
declare-copatternω : ∀ {U : Typeω} → Name → U → TC ⊤
declare-copatternω nm A = do
`A ← quoteωTC A
-- Cannot quote things in type Typeω, but we can infer their type.
`U ← infer-type `A
make-copattern true nm `A `U
define-copatternω
: (nm : Name) {@(tactic (type-for "copatternω" false nm)) U : Typeω}
→ U → TC ⊤
define-copatternω nm A = do
`U ← get-type nm
`A ← define-abbrev nm "value" `U =<< quoteωTC A
make-copattern false nm `A `U
{-
Another common pattern is that two records `r : R p` and `s : R q` may contain
the same data, but they are parameterized by different values.
The `define-eta-expansion` macro will automatically construct a function
`R p → R q` that eta-expands the first record out into a copattern definition.
-}
define-eta-expansion : Name → TC ⊤
define-eta-expansion nm = do
tp ← reduce =<< infer-type (def nm [])
let tele , _ = pi-view tp
let tm = tel→lam tele (var 0 [])
make-copattern false nm tm tp
--------------------------------------------------------------------------------
-- Tests
private module Test where
-- Record type that uses all interesting features.
record Record {ℓ} (A : Type ℓ) : Type ℓ where
no-eta-equality
constructor mk
field
⦃ c ⦄ : A
{ f } : A → A
const : ∀ {x} → f x ≡ c
-- Record created via a constructor.
via-ctor : Record Nat
via-ctor = mk ⦃ c = 0 ⦄ {f = λ _ → 0} refl
-- Both macros work.
unquoteDecl copat-decl-via-ctor = declare-copattern copat-decl-via-ctor via-ctor
copat-def-via-ctor : Record Nat
unquoteDef copat-def-via-ctor = define-copattern copat-def-via-ctor via-ctor
-- Record created by a function.
module _ (r : Record Nat) where
open Record r
via-function : Record Nat
via-function .c = suc c
via-function .f = suc ∘ f
via-function .const = ap suc const
-- Also works when applied to the result of a function.
unquoteDecl copat-decl-via-function = declare-copattern copat-decl-via-function (via-function via-ctor)
-- Test to see how we handle unused parameters.
record Unused (n : Nat) : Type where
field
actual : Nat
zero-unused-param : Unused 0
zero-unused-param = record { actual = 0 }
one-unused-param : ∀ {n} → Unused n
unquoteDef one-unused-param = declare-copattern one-unused-param zero-unused-param
-- This is a type error:
-- unquoteDef one-unused-param = define-copattern one-unused-param zero-unused-param
-- because the 'define' macro propagates the type of the thing being
-- defined inwards.
-- Functions into records that are universe polymorphic
neat : ∀ {ℓ} {A : Type ℓ} → A → Record A
neat a .Record.c = a
neat a .Record.f _ = a
neat a .Record.const = refl
-- Implicit insertion is correct for the define- macro, since the type
-- of the function is given.
cool : ∀ {ℓ} {A : Type ℓ} → A → Record A
unquoteDef cool = define-copatternω cool neat
-- Eta-expanders
expander : ∀ {m n : Nat} → Unused m → Unused n
unquoteDef expander = define-eta-expansion expander
-- Raises a type error: the function should have a declaration.
-- unquoteDecl uncool = define-copatternω uncool neat