`copilot-core`: Facilitate use of structs
Closed this issue · 9 comments
Description
Currently, using structs in Copilot requires defining instances of several type classes. Defining those instances is unnecessarily cumbersome, and it exposes the user to a substantial amount of Haskell even when they want to stay at the level of the Copilot language.
It would be helpful to facilitate generating instances of Typed
and Struct
for struct types / record types, by providing functions that implement the default behavior and only requiring users to write their own functions if they wanted to deviate from those defaults.
Type
- Feature: Improve usability of the language.
Additional context
None.
Requester
- Ivan Perez.
Method to check presence of bug
Not applicable (not a bug).
Expected result
Introduce functions that implement the default behavior for the methods in the Typed
and Struct
type classes, so that users can rely on them when implementing their own instances.
Desired result
Introduce functions that implement the default behavior for the methods in the Typed
and Struct
type classes, so that users can rely on them when implementing their own instances.
Proposed solution
Introduce four definitions in copilot-core:Copilot.Core.Type
that provide default implementations for the fields in the Typed
and Struct
type classes for types that are Generic
. Let users use those types so long as they make their structs Generic
.
Add an example to the examples directory in copilot
that shows how the new implementations can be used.
Further notes
Discussion #540 proposes multiple alternative implementations. Although the preferred solution long-term is 2, the mechanism proposed is not backwards compatible, so it helps to first introduce a mechanism to simplify the implementation without breaking existing code. We can, upon release of this feature, communicate to users that Copilot will move in that direction if appropriate, and encourage users to transition their code so that we can introduce breaking changes 3 versions later per our deprecation policy.
Change Manager: Confirmed that the issue exists.
Technical Lead: Confirmed that the issue should be addressed.
Technical Lead: Issue scheduled for fixing in Copilot 4.2
Fix assigned to: @RyanGlScott .
Just to be explicit about what I am planning to do for the next release (and to relate it back to the original discussion in #540), my plan is to add the following four functions to Copilot.Core.Type
:
typeNameDefault :: (Generic a, ...) => a -> String
toValuesDefault :: (Generic a, ...) => a -> [Value a]
updateFieldDefault :: (Generic a, ...) => a -> Value t -> a
typeOfDefault :: (Typed a, Struct a, Generic a, ...) => Type a
Note the following differences from the proposal in #540:
- The naming convention is slightly different: I've moved "default" to the end of each function instead of at the beginning.
- There is now a default for
typeName
. This differs from #540, where I envisioned that users would always supply the generated struct name manually. After talking about this with @ivanperez-keera, however, we came to the consensus that most of the time, it would be more convenient just to generate atypeName
that converts the name of the Haskell datatype to snake_case (e.g., convert the nameFooBar
tofoo_bar
). This is not the only possible way to do it, but we can always add alternative ways to do it later. - In light of having a dedicated
typeNameDefault
function, I propose not adding aGenericStruct
newtype. It was mainly useful to allow users to easily specify the generated type name in conjunction withderiving
, but now that we have a reasonable default fortypeName
, users won't need to perform this extra step in the first place.
Thank you, @RyanGlScott . This looks good.
Please let me know when you start working on this so that I switch it to "Implementation".
I have a partial implementation of this (based on the work in #516). I'm currently cleaning it up and documenting it, so feel free to switch this to "Implementation".
Implementor: Solution implemented, review requested.
Change Manager: Verified that:
- Solution is implemented:
- The code proposed compiles and passes all tests. Details:
Build log: https://app.travis-ci.com/github/Copilot-Language/copilot/builds/273752793 - The solution proposed produces the expected result. Details:
The following Dockerfile checks that the new methods can be used in place of the hand-coded implementations of theStruct
andTyped
class methods, by comparing the results produced against known expectations, and it also that the examples updated to illustrate the new features compile successfully, after which it prints the message "Success":Command (substitute variables based on new path after merge):--- Dockerfile FROM ubuntu:focal ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install --yes libz-dev RUN apt-get install --yes git RUN apt-get install --yes wget RUN mkdir -p $HOME/.ghcup/bin RUN wget https://downloads.haskell.org/~ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 -O $HOME/.ghcup/bin/ghcup RUN chmod a+x $HOME/.ghcup/bin/ghcup ENV PATH=$PATH:/root/.ghcup/bin/ ENV PATH=$PATH:/root/.cabal/bin/ RUN apt-get install --yes curl RUN apt-get install --yes gcc g++ make libgmp3-dev RUN apt-get install --yes pkg-config RUN apt-get install --yes z3 SHELL ["/bin/bash", "-c"] RUN ghcup install ghc 9.4 RUN ghcup install cabal 3.2 RUN ghcup set ghc 9.4.8 RUN cabal update ADD Structs.hs /tmp/Structs.hs ADD main.c /tmp/main.c ADD expected /tmp/expected CMD git clone $REPO \ && cd $NAME \ && git checkout $COMMIT \ && cabal v1-sandbox init \ && cabal v1-install alex happy --constraint='happy < 2' \ && cabal v1-install copilot**/ \ && cabal v1-exec -- runhaskell /tmp/Structs.hs \ && mv /tmp/main.c . \ && gcc main.c structs.c -o main \ && ./main > actual \ && diff actual /tmp/expected \ && cabal v1-exec -- runhaskell copilot/examples/Structs.hs \ && cabal v1-exec -- runhaskell copilot/examples/StructsUpdateField.hs \ && cabal v1-exec -- runhaskell copilot/examples/what4/Structs.hs \ && echo Success --- Structs.hs {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import Copilot.Compile.C99 import Data.Type.Equality as DE import Language.Copilot import GHC.Generics -- * Struct type definitions data SoA = SoA { arr :: Field "arr" SoB } deriving Generic instance Struct SoA where typeName = typeNameDefault toValues = toValuesDefault instance Typed SoA where typeOf = typeOfDefault data SoB = SoB { arr2 :: Field "arr2" (Array 3 Float) } deriving Generic instance Struct SoB where typeName = typeNameDefault toValues = toValuesDefault instance Typed SoB where typeOf = typeOfDefault data SoC = SoC { arr3 :: Field "arr3" Int32 } deriving Generic -- | Struct with a custom value for updates instance Struct SoC where typeName = typeNameDefault toValues = toValuesDefault updateField = updateFieldDefault instance Typed SoC where typeOf = typeOfDefault -- Sample values -- We purposefully leave some type signatures out to check that GHC can infer -- them. -- SoAs a1 = SoA $ Field $ SoB $ Field $ array [0, 1, 2] b1 = SoB $ Field $ array [10, 20, 30] b2 = SoB $ Field $ array [40, 50, 60] c1 = SoC $ Field 5 soa :: Stream SoA soa = constant a1 soa1 = soa ## arr =: recursiveArray soa2 = soa ## arr =: constant b1 soarr = soa ## arr =: soa # arr soc1 = constant c1 ## arr3 =: counter soc2 = constant c1 ## arr3 =$ (+5) recursiveArray :: Stream SoB recursiveArray = [b1, b2] ++ recursiveArray counter :: Stream Int32 counter = [55] ++ (counter + 1) spec :: Spec spec = do trigger "arrays" (soa1 # arr # arr2 ! 1 /= 60) [arg soa, arg soa1, arg soa2] trigger "arrays2" true [arg soc1] trigger "arrays3" true [arg soc2] main :: IO () main = do spec' <- reify spec compile "structs" spec' --- main.c #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include "structs.h" #include "structs_types.h" void arrays( struct so_a * arrays_arg0 , struct so_a * arrays_arg1 , struct so_a * arrays_arg2 ) { printf("0: %f %f %f\n" , arrays_arg0->arr.arr2[0] , arrays_arg0->arr.arr2[1] , arrays_arg0->arr.arr2[2]); printf("1: %f %f %f\n" , arrays_arg1->arr.arr2[0] , arrays_arg1->arr.arr2[1] , arrays_arg1->arr.arr2[2]); printf("2: %f %f %f\n" , arrays_arg2->arr.arr2[0] , arrays_arg2->arr.arr2[1] , arrays_arg2->arr.arr2[2]); } void arrays2(struct so_c * arrays2_arg0) { printf("soc1: %d\n", arrays2_arg0->arr3); } void arrays3(struct so_c * arrays2_arg0) { printf("soc2: %d\n", arrays2_arg0->arr3); } int main() { step(); step(); step(); step(); } --- expected 0: 0.000000 1.000000 2.000000 1: 10.000000 20.000000 30.000000 2: 10.000000 20.000000 30.000000 soc1: 55 soc2: 10 0: 0.000000 1.000000 2.000000 1: 40.000000 50.000000 60.000000 2: 10.000000 20.000000 30.000000 soc1: 56 soc2: 10 0: 0.000000 1.000000 2.000000 1: 10.000000 20.000000 30.000000 2: 10.000000 20.000000 30.000000 soc1: 57 soc2: 10 0: 0.000000 1.000000 2.000000 1: 40.000000 50.000000 60.000000 2: 10.000000 20.000000 30.000000 soc1: 58 soc2: 10
$ docker run -e "REPO=https://github.com/GaloisInc/copilot-1" -e "NAME=copilot-1" -e "COMMIT=cbab0cca637753b68d0d775095ab97f5a87937e0" -it copilot-verify-564
- The code proposed compiles and passes all tests. Details:
- Implementation is documented. Details:
The new classes, instances and examples all include documentation. - Change history is clear.
- Commit messages are clear.
- Changelogs are updated.
- Examples are updated. Details:
New examples are included, and existing examples are updated to use the new, simpler methods. - Required version bumps are evaluated. Details:
No bump required; the new features are backwards compatible.
Change Manager: Implementation ready to be merged.