Local Rewriting

Local rewrite rules is an experimental feature which enables parameterising modules over computation rules. Specifically, it allows declaring module parameters targetting a rewrite relation as local rewrite rules by annotating with them the @rewrite attribute. Consequently:

  • Inside the module, local rewrite rules will automatically apply during reduction, rewriting instances of the left-hand side to the right-hand side, similarly to global rewriting.

  • Outside the module, local rewrite rules act as constraints on instantiations of the module parameters. E.g. when opening the module, Agda will check that both sides are definitionally equal.

This feature is based on Local Rewriting Type Theory (LRTT) as introduced in Encode the Cake and Eat It Too, by Yann Leray and Théo Winterhalter. Unlike their presentation, we do not make a strong syntactic distinction between the “interface environment” and the “local context”, but nonetheless, by restricting @rewrite attributes to module parameters, quantification over rewrites is prenex-only. For example, definitions cannot return local rewrite rules, or be parameterised over other definitions taking local rewrite rules, so foo : (n : Nat) ((@rewrite p : n 0) Nat) Nat is not allowed.

Semantically, local rewrite rules can be eliminated by inlining all instantiations of modules with local rewrite rule parameters and so should be conservative over the rest of Agda’s theory.

Note

This page is about the --local-rewriting option. This is is unrelated to --local-confluence-check, which enables a form of confluence checking for global REWRITE rules whilst using the --rewriting option.

It is also (currently) distinct from the the rewrite construct, although there are plans to implement a new version of with-abstraction which internally desugars to local rewrite rules (“smart with”).

Local rewrite rules by example

{-# OPTIONS --local-rewriting --rewriting #-}

module language.local-rewriting where

open import Agda.Builtin.Equality
open import Agda.Builtin.Equality.Rewrite

To motivate local rewrite rules, consider the following code which implements addition and proves associativity for Agda’s built-in natural numbers.

module Addition where
  _+_ : Nat  Nat  Nat
  zero  + m = m
  suc n + m = suc (n + m)

  +-assoc :  {n m l}  (n + m) + l  n + (m + l)
  +-assoc {n = zero}  = refl
  +-assoc {n = suc n} = cong suc (+-assoc {n = n})

Now imagine we want to use a different encoding of natural numbers - for example, lists of unit values.

Nat' = List 

To define addition and prove associativity for Nat' without duplication, we can parameterise our addition and the associativity definitions over an abstract type of natural numbers and an induction principle.

module ParametricAddition
  (Nat : Set) (zero : Nat) (suc : Nat  Nat)
  (ind : (P : Nat  Set)  P zero  ( n  P n  P (suc n))   n  P n)
  (ind-zero :  {P z s}  ind P z s zero  z)
  (ind-suc  :  {P z s n}  ind P z s (suc n)  s n (ind P z s n))
  where
  _+_ : Nat  Nat  Nat
  n + m = ind (λ _  Nat) m (λ _  suc) n

  +-assoc :  {n m l}  (n + m) + l  n + (m + l)
  +-assoc {n = n} {m = m} {l = l}
    = ind (λ   ( + m) + l   + (m + l))
          (trans (cong (_+ l) ind-zero) (sym ind-zero))
          (λ _ h  trans (cong (_+ l) ind-suc)
                 ( trans ind-suc
                 ( trans (cong suc h) (sym ind-suc))))
          n

We have succeeded in writing a single parametric definition of addition and associativity, but at the cost of a much more convoluted proof. The parameterised-over induction principle no longer computes on zero and successor automatically, so we have to manually invoke ind-zero and ind-suc multiple times.

Local rewrite rules resolve this tedium. We can simply annotate the ind-zero and ind-suc equations with @rewrite and recover the simple associativity proof, whilst staying parametric over encoding details.

module ParametricAdditionRew
  (Nat : Set) (zero : Nat) (suc : Nat  Nat)
  (ind : (P : Nat  Set)  P zero  ( n  P n  P (suc n))   n  P n)
  (@rewrite ind-zero :  {P z s}  ind P z s zero  z)
  (@rewrite ind-suc  :  {P z s n}  ind P z s (suc n)  s n (ind P z s n))
  where
  _+_ : Nat  Nat  Nat
  n + m = ind (λ _  Nat) m (λ _  suc) n

  +-assoc :  {n m l}  (n + m) + l  n + (m + l)
  +-assoc {n = n} {m = m} {l = l}
    = ind (λ   ( + m) + l   + (m + l)) refl (λ _ h  cong suc h) n

For more examples on where local rewrite rules can be useful, see the Encode the Cake and Eat It Too paper.

Limitations

Confluence and termination checking

Currently, no form of confluence or termination checking is implemented for local rewrite rules. The consequences of non-confluent or non-terminating local rewrite rules are similar to global rewriting: non-confluence endangers subject reduction and non-termination might cause the typechecker to loop, but logical soundness should never be threatened.

Refining local rewrite rules with pattern matching

Currently, inaccessible pattern matches on variables that occur in local rewrite rules are not allowed. This is to avoid typechecking under invalid rewrite rules (rewrite validity is unstable under substitution).

module _ (f : Nat  Nat) (@rewrite p : f 1  0) where
  bad : f  (λ x  x)  Nat
  bad refl = {!!} -- Substituting 'f' for 'λ x → x' here would invalidate the
                  -- local rewrite rule 'p'

Furthermore, matches on variables in local rewrite rules breaks the inlining-based semantic justification.

In spite of these downsides, there are plans to relax this restriction in the future under an additional flag. Refining local rewrite rules with pattern matches enables a restricted form of “local equality reflection”, which has many interesting applications, including a (hopefully) better-behaved with-abstraction mechanism.

Parameterising datatypes over local rewrite rules with --cubical

In Cubical Agda, there is an additional limitation with local rewrite rules. Attempting to declare data or record types inside modules with local rewrite rule parameters will throw a CannotGenerateTransportLocalRewrite error:

module _ (n : Nat) (@rewrite _ : n  0) where
  data Foo : Set where
    mk : Foo

This restriction is due to how Cubical Agda automatically defines various primitives for datatypes for transport and path composition. It is not (currently) clear what these should look like for datatypes with local rewrite rule parameters.

If you run into this issue, try defining your data or record types outside of the module with the local rewrite rule.