agda / agda2hs

Compiling Agda code to readable Haskell
https://agda.github.io/agda2hs
MIT License
177 stars 37 forks source link

Release agda2hs 1.3 #366

Closed jespercockx closed 1 month ago

jespercockx commented 1 month ago

It seems like we're long overdue for a new release (as indicated by the comment by @HeinrichApfelmus in https://github.com/agda/agda2hs/issues/361#issuecomment-2364047031_).

One thing that might be worth doing is to investigate some lower/upper bounds on the dependencies (see e.g. https://github.com/agda/agda2hs/issues/347 and https://github.com/agda/agda2hs/pull/350#issuecomment-2363512179).

jespercockx commented 1 month ago

I've uploaded a release candidate to https://hackage.haskell.org/package/agda2hs-1.3/candidate

It would be great if people could give it a test run (perhaps @omelkonian @HeinrichApfelmus @anka-213). If there are no complaints I'll do a proper release next week.

HeinrichApfelmus commented 1 month ago

It would be great if people could give it a test run

Do you have a git commit (hash) that this release corresponds to?

I need to test the combination of agda2hs executable and agda2hs.agda-lib Agda library. If I understand correctly, only the executable is released on Hackage? šŸ¤” I'm asking for a git commit, because that would allow me to get the combination as presented in a specific flake.nix.

jespercockx commented 1 month ago

It should correspond to https://github.com/agda/agda2hs/commit/770f209c3e1aa94ba9d34c1488379e83190d589d

jespercockx commented 1 month ago

If I understand correctly, only the executable is released on Hackage?

I think that's the case, indeed. I'm not sure if there's an easy way to release the library also on Hackage, do you think it's worth investigating or is it fine to just distribute it via github?

HeinrichApfelmus commented 1 month ago

do you think it's worth investigating or is it fine to just distribute it via github?

Distribution via Github, specifically the flake.nix works for me.

For a "proper" release that is accessible to a larger audience than just me, I think that investing time into creating release artifacts is unavoidable. I suppose that

would do the trick.

More generally, in order to make agda2hs "the missing proof assistant for Haskell", there is probably no way around integrating with .cabal.

jespercockx commented 1 month ago

I can definitely add the library to the data-files field. However, I'm not sure if I understand how the second point would work. I don't think it would be a good idea to let agda2hs silently override what's in the user's configuration in their libraries file or in the agda-lib file of their project.

Perhaps an alternative would be to provide a simple executable that sets up a new agda2hs project that automatically includes the agda2hs library that was installed through Cabal?

omelkonian commented 1 month ago

It would be great if people could give it a test run

I installed locally (using cabal) and everything works fine, including using the agda2hs executable interactively in Emacs.

HeinrichApfelmus commented 1 month ago

I don't think it would be a good idea to let agda2hs silently override what's in the user's configuration in their libraries file or in the agda-lib file of their project.

Fair enough, I agree. Though, since agda2hs is not agda, doing that does not necessarily count as "silent".

Perhaps an alternative would be to provide a simple executable that sets up a new agda2hs project that automatically includes the agda2hs library that was installed through Cabal?

The main difficulty is to bring the agda2hs library in scope ā€” only cabal install knows where it is going to install the data-files.

But the agda2hs will be told about the location, so the simplest solution may be to add a command to agda2hs that print outs the location of the library?

I personally don't like generated project templates, it's not modular enough for my taste. I would use it to find the location of the library, and then delete the rest of the generated project. šŸ˜…

jespercockx commented 1 month ago

But the agda2hs will be told about the location, so the simplest solution may be to add a command to agda2hs that print outs the location of the library?

That also seems a lot more feasible, so I'm all in favor.

HeinrichApfelmus commented 1 month ago

In order to test out agda2hs-1.3, I have attempted to update the codebase cardano-wallet-agda to use the new version.

However, I ran into an unexpected issue:

I have a type PairMap consisting of two Data.Map and an invariant that they both contain the same data, albeit in different order. Specifically,

record PairMap (a b v : Set) {{orda : Ord a}} {{ordb : Ord b}} : Set where
  field
    mab : Map a (Map b v)
    mba : Map b (Map a v)

    @0 invariant-equal
      : āˆ€ (x : a) (y : b)
      ā†’ lookup2 x y mab ā‰” lookup2 y x mba

In order to formulate the invariant, I needed to constrain the type parameters a and b to have Ord instances, otherwise I cannot use the lookup2 function.

However, agda2hs version 1.3 now gives me an error message when compiling:

ā€¦/lib/customer-deposit-wallet-pure/agda/Haskell/Data/Maps/PairMap.agda:400,8-15
Constraint in type parameter not supported: (orda : Ord a)

How to proceed?

I have tried moving the instance arguments to the definition of invariant-equal, but then I get the following error when trying to check a proof:

No instance of type Monad _m_2920 was found in scope.
- iMonadMaybe was ruled out because
  /Users/hgz/Documents/MeineDokumente/Programmierung/Haskell/Work/cardano-wallet-agda/lib/customer-deposit-wallet-pure/agda/Haskell/Data/Maps/PairMap.agda:425,15-60
  Cannot instantiate the metavariable _2870 to solution ordb
  since it contains the variable ordb
  which is not in scope of the metavariable
  when checking that the inferred type of an application
    (iMonadMaybe Monad.>>= Map.lookup y Map.empty)
    (Ī» vā‚ ā†’ Map.lookup x vā‚)
    ā‰” (iMonadMaybe Monad.>>= Nothing) (Ī» vā‚ ā†’ Map.lookup x vā‚)
  matches the expected type
    (iMonadMaybe Monad.>>= Map.lookup y Map.empty) (Map.lookup x) ā‰”
    (_r_2921 Monad.>>= Nothing) (Map.lookup x)
jespercockx commented 1 month ago

@HeinrichApfelmus The instance constraints Ord a and Ord b are not supposed to show up in the generated Haskell code, are they? In other words, did you really want to write the following Haskell type?

data (Ord a, Ord b) => PairMap a b v = PairMap { mab :: Map a (Map b v)); mba :: (Map b (Map a v)) }

Note that this is only valid with the deprecated -XDatatypeContexts flag (see https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/datatype_contexts.html), and currently not supported by agda2hs.

In previous versions of agda2hs, the constraints on the datatype would instead be dropped, even though they did not have an erasure annotation. If you want to restore this old behavior, all you have to do is to add the erasure annotations:

record PairMap (a b v : Set) {{@0 orda : Ord a}} {{@0 ordb : Ord b}} : Set where ...
HeinrichApfelmus commented 1 month ago

The instance constraints Ord a and Ord b are not supposed to show up in the generated Haskell code, are they? [ā€¦] If you want to restore this old behavior, all you have to do is to add the erasure annotations:

Thank you, the erasure annotations are exactly what I need, indeed. šŸ™

The use case is worth keeping in mind: Even though the computational parts of the data type do not depend on the type class, the (erased) invariants can only be expressed with the type class constraints.

HeinrichApfelmus commented 1 month ago

I have another issue: The following module

module Test where

open import Haskell.Prelude

test : Integer
test = fromMaybe 0 (Just 12)

{-# COMPILE AGDA2HS test #-}

is transpiled to

module Test where

test :: Integer
test = fromMaybe 0 (Just 12)

but this module does not compile ā€” it's missing a line

import Data.Maybe (fromMaybe)

In other words, the function fromMaybe happens to not be a part of the Haskell Prelude.

HeinrichApfelmus commented 1 month ago

I also have an issue where a type class is transpiled as an ordinary argument. šŸ¤”

Specifically, consider the following definition:

Byron = āŠ¤

postulate
  IsEra : Set ā†’ Set
  instance
    iIsEraByron : IsEra Byron

The intention is that the class IsEra is defined in a Haskell module, and imported via a postulate. It's essentially a singleton class.

In agda2hs-1.2, the definition

-- adga2hs-1.2
utxoFromTxOutputs : āˆ€{era} ā†’ {{IsEra era}} ā†’ Read.Tx era ā†’ UTxO
utxoFromTxOutputs = utxoFromEraTx

was happily transpiled to

-- adga2hs-1.3
utxoFromTxOutputs :: IsEra era => Tx era -> UTxO
utxoFromTxOutputs = utxoFromEraTx

However, in agda2hs-1.3, this definition is transpiled to

utxoFromTxOutputs :: IsEra era -> Tx era -> UTxO
utxoFromTxOutputs x = utxoFromEraTx x

which the Haskell compiler will not accept, because IsEra is a type class, not a data type.

EDIT: It looks like defining IsEra as a data type does not change the transpilation output.

flupe commented 1 month ago

The use case is worth keeping in mind: Even though the computational parts of the data type do not depend on the type class, the (erased) invariants can only be expressed with the type class constraints.

Indeed, it's actually a design pattern that shows up in the very first example in the documentation!

data BST (a : Set) {{@0 _ : Ord a}} (@0 lower upper : a) : Set where
  Leaf : (@0 pf : lower ā‰¤ upper) ā†’ BST a lower upper
  Node : (x : a) (l : BST a lower x) (r : BST a x upper) ā†’ BST a lower upper

In other words, the function fromMaybe happens to not be a part of the Haskell Prelude.

Good catch, related issue to #245.

I also have an issue where a type class is transpiled as an ordinary argument.

Also a good catch! Since #284 we do check if the type of the instance argument is compiled to a class or not, and if not it is added as a regular argument. Maybe we should then add a specific compile pragma for postulated type formers that says "this is actually a Haskell class". Will open an issue for that.

HeinrichApfelmus commented 1 month ago

Maybe we should then add a specific compile pragma for postulated type formers that says "this is actually a Haskell class". Will open an issue for that.

Yes, please. This issue is currently preventing me from upgrading my codebase to agda2hs-1.3. šŸ˜…

flupe commented 1 month ago

Wait, now that I think about it we already have the following pragma:

{-# COMPILE AGDA2HS IsEra existing-class #-}

used extensively in the prelude to avoid compiling the records. So it definitely works on records, could you please check if this also works in your codebase for postulated typeclasses?

HeinrichApfelmus commented 1 month ago

So it definitely works on records, could you please check if this also works in your codebase for postulated typeclasses?

Unfortunately, it does not appear to work on postulate. šŸ¤” Fortunately, converting my singleton type class to a record is worth the duplication, so at least this issue is not blocking me.

HeinrichApfelmus commented 1 month ago

One last thing that I noticed:

I have two definitions

member
    : āˆ€ {time} {{_ : Ord time}}
    ā†’ time ā†’ RollbackWindow time ā†’ Bool
member time w = (finality w <= time) && (time <= tip w)

prune
    : āˆ€ {time} {{_ : Ord time}}
    ā†’ time ā†’ RollbackWindow time ā†’ Maybe (RollbackWindow time)
prune newFinality w =
  if member newFinality w
    then (Ī» {{cond}} ā†’ Just (record
      { tip = tip w
      ; finality = newFinality
      ; invariant = projr (prop-&&-ā‹€ cond)
      }))
    else Nothing

in my codebase. Strangely, the definition of prune is transpiled to

prune newFinality w =
    if finality w <= newFinality && newFinality <= tip w
        then Just (RollbackWindowC newFinality (tip w))
        else Nothing

instead of

prune newFinality w =
    if member newFinality w
        then Just (RollbackWindowC newFinality (tip w))
        else Nothing

In other words, the application of member is inlined in the transpilation output. šŸ˜² This is fine from a correctness perspective, but unfortunately undesirable as far as transpilation goes, where we would like to preserve the original code as much as possible. Inlining a top-level definition is certainly something one would not want to do in transpilation.

flupe commented 1 month ago

Unfortunately, it does not appear to work on postulate.

Right, the culprit here seems to be this line: https://github.com/agda/agda2hs/blob/0412c42844b94d4d687d8bba3a29e7e815c93a2b/src/Agda2Hs/Compile/Utils.hs#L170

Where we require Records for class candidates. I think it should be fine to allow Axioms too specifically for ExistingClass, provided we check somewhere that the postulate is indeed a good class type former.

One last thing that I noticed: [...]

Could you show the compile pragmas you used for prune and member? I find it surprising that member would be treated as inline, though it wouldn't be the first time. Do you think you could find a minimal standalone example?

jespercockx commented 1 month ago

Could you show the compile pragmas you used for prune and member? I find it surprising that member would be treated as inline, though it wouldn't be the first time. Do you think you could find a minimal standalone example?

Assuming that you didn't use a {-# COMPILE AGDA2HS member inline #-} pragma, the other potential culprit could be that you have the --auto-inline flag enabled or an {-# INLINE member #-} pragma.

HeinrichApfelmus commented 1 month ago

I didn't enable any pragmas.

The following appears to be a minimal test case:

./agda/Test.agda

{-# OPTIONS --erasure #-}

module Test where

open import Haskell.Prelude

{-----------------------------------------------------------------------------
    RollbackWindow
------------------------------------------------------------------------------}
record RollbackWindow (time : Set) : Set where
  constructor RollbackWindowC
  field
    finality : time
    tip : time

open RollbackWindow public

member
    : āˆ€ {time} {{_ : Ord time}}
    ā†’ time ā†’ RollbackWindow time ā†’ Bool
member time w = (finality w <= time) && (time <= tip w)

prune
    : āˆ€ {time} {{_ : Ord time}}
    ā†’ time ā†’ RollbackWindow time ā†’ Maybe (RollbackWindow time)
prune newFinality w =
  if member newFinality w
    then (Ī» {{cond}} ā†’ Just (record
      { tip = tip w
      ; finality = newFinality
      }))
    else Nothing

{-# COMPILE AGDA2HS RollbackWindow #-}
{-# COMPILE AGDA2HS member #-}
{-# COMPILE AGDA2HS prune #-}

./mylib.agda-lib

name: mylib
include: agda
depend:
  agda2hs
flags: -W noUnsupportedIndexedMatch --erasure

and then the invocation

$ agda2hs ./agda/Test.agda
Writing [ā€¦]/agda/Test.hs

will produce the file

./agda/Test.hs

module Test where

data RollbackWindow time = RollbackWindowC{finality :: time,
                                           tip :: time}

member :: Ord time => time -> RollbackWindow time -> Bool
member time w = finality w <= time && time <= tip w

prune ::
        Ord time =>
        time -> RollbackWindow time -> Maybe (RollbackWindow time)
prune newFinality w
  = if finality w <= newFinality && newFinality <= tip w then
      Just (RollbackWindowC newFinality (tip w)) else Nothing

where the function member has been inlined in the definition of prune.


I noticed that removing the type variable time and using a concrete type Time = Integer everywhere will not produce the unexpected inlining.

jespercockx commented 1 month ago

Thank you for the report, I've investigated it and created a new issue in https://github.com/agda/agda2hs/issues/376. For now, it can be worked around by disabling the projection-likeness optimization with the --no-projection-like flag.

jespercockx commented 1 month ago

I've updated the release candidate at https://hackage.haskell.org/package/agda2hs-1.3/candidate to correspond to the current master (https://github.com/agda/agda2hs/commit/afb539213772c88f45914b0fbc49df6917ad4701). Since none of the remaining issues are critical, I plan to release this version tomorrow.

HeinrichApfelmus commented 1 month ago

Since none of the remaining issues are critical, I plan to release this version tomorrow.

Sounds good to me, thank you!

jespercockx commented 1 month ago

Agda2hs 1.3 has now been officially released! https://hackage.haskell.org/package/agda2hs-1.3