Skip to content

Commit 6cf3581

Browse files
Merge remote-tracking branch 'galois/develop-generic-struct-typed-methods'. Close #564.
**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. The following Dockerfile checks that the new methods can be used in place of the hand-coded implementations of the Struct and Typed 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": ``` --- 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 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 ``` **Solution implemented** 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`. Update the examples in 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.
2 parents 8f783ee + cbab0cc commit 6cf3581

File tree

6 files changed

+261
-83
lines changed

6 files changed

+261
-83
lines changed

copilot-core/CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
2024-12-29
1+
2025-01-06
22
* Deprecate fields of Copilot.Core.Expr.UExpr. (#565)
33
* Increase test coverage. (#555)
4+
* Define generic implementations of Struct and Typed methods. (#564)
45

56
2024-11-07
67
* Version bump (4.1). (#561)

copilot-core/src/Copilot/Core/Type.hs

Lines changed: 195 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
{-# LANGUAGE FlexibleInstances #-}
55
{-# LANGUAGE GADTs #-}
66
{-# LANGUAGE KindSignatures #-}
7-
{-# LANGUAGE Safe #-}
87
{-# LANGUAGE ScopedTypeVariables #-}
8+
{-# LANGUAGE Trustworthy #-}
9+
{-# LANGUAGE TypeApplications #-}
910
{-# LANGUAGE TypeOperators #-}
1011
{-# LANGUAGE UndecidableInstances #-}
1112
-- |
@@ -20,6 +21,7 @@
2021
module Copilot.Core.Type
2122
( Type (..)
2223
, Typed (..)
24+
, typeOfDefault
2325
, UType (..)
2426
, SimpleType (..)
2527

@@ -28,25 +30,32 @@ module Copilot.Core.Type
2830

2931
, Value (..)
3032
, toValues
33+
, toValuesDefault
3134
, Field (..)
3235
, typeName
36+
, typeNameDefault
3337

3438
, Struct
3539
, fieldName
3640
, accessorName
3741
, updateField
42+
, updateFieldDefault
3843
)
3944
where
4045

4146
-- External imports
47+
import Data.Char (isLower, isUpper, toLower)
48+
import Data.Coerce (coerce)
4249
import Data.Int (Int16, Int32, Int64, Int8)
4350
import Data.List (intercalate)
4451
import Data.Proxy (Proxy (..))
4552
import Data.Type.Equality as DE
4653
import Data.Typeable (Typeable, eqT, typeRep)
4754
import Data.Word (Word16, Word32, Word64, Word8)
55+
import GHC.Generics (Datatype (..), D1, Generic (..), K1 (..), M1 (..),
56+
U1 (..), (:*:) (..))
4857
import GHC.TypeLits (KnownNat, KnownSymbol, Symbol, natVal, sameNat,
49-
symbolVal)
58+
sameSymbol, symbolVal)
5059

5160
-- Internal imports
5261
import Copilot.Core.Type.Array (Array)
@@ -61,24 +70,35 @@ class Struct a where
6170
toValues :: a -> [Value a]
6271

6372
-- | Update the value of a struct field. This is only used by the Copilot
64-
-- interpreter, so if you do not plan to use the interpreter, it is safe to
65-
-- omit an implementation of this method.
73+
-- interpreter.
6674
--
67-
-- In order to implement 'updateField', you should do the following for each
68-
-- 'Field' in a struct:
75+
-- If you do not plan to use the interpreter, you can omit an implementation
76+
-- of this method. If you do so, it is recommended that you derive a 'Generic'
77+
-- instance for the struct data type. This is because in a future release, the
78+
-- default implementation of 'updateField' (which will be picked if there is
79+
-- not a manually written implementation) will be changed to require a
80+
-- 'Generic' instance.
6981
--
70-
-- 1. Check that the name of the 'Field' matches the name of the supplied
71-
-- 'Value' (using 'GHC.TypeLits.sameSymbol').
82+
-- In order to implement 'updateField', pick one of the following approaches:
7283
--
73-
-- 2. Check that the type of the 'Field' matches the 'Type' of the supplied
74-
-- 'Value' (using 'DE.testEquality').
84+
-- * Derive a 'Generic' instance for the struct data type and then define
85+
-- @'updateField' = 'updateFieldDefault'@ in the 'Struct' instance.
7586
--
76-
-- 3. If both (1) and (2) succeed, update the corresponding struct field using
77-
-- a record update.
87+
-- * Manually implement 'updateField' by doing the following for each 'Field'
88+
-- in a struct:
7889
--
79-
-- For a complete end-to-end example that demonstrates how to implement
80-
-- 'updateField' and use it in the Copilot interpreter, see the
81-
-- @examples/StructsUpdateField.hs@ example in the @copilot@ library.
90+
-- 1. Check that the name of the 'Field' matches the name of the supplied
91+
-- 'Value' (using 'GHC.TypeLits.sameSymbol').
92+
--
93+
-- 2. Check that the type of the 'Field' matches the 'Type' of the supplied
94+
-- 'Value' (using 'DE.testEquality').
95+
--
96+
-- 3. If both (1) and (2) succeed, update the corresponding struct field
97+
-- using a record update.
98+
--
99+
-- For a complete end-to-end example that demonstrates how to manually
100+
-- implement 'updateField' and use it in the Copilot interpreter, see the
101+
-- @examples/StructsUpdateField.hs@ example in the @copilot@ library.
82102
updateField :: a -> Value t -> a
83103
updateField = error $ unlines
84104
[ "Field updates not supported for this type."
@@ -272,3 +292,163 @@ data UType = forall a . Typeable a => UType { uTypeType :: Type a }
272292

273293
instance Eq UType where
274294
UType ty1 == UType ty2 = typeRep ty1 == typeRep ty2
295+
296+
-- * GHC.Generics-based defaults
297+
298+
-- | A default implementation of 'typeName' that leverages 'Generic'. In order
299+
-- to use this, make sure you derive a 'Generic' instance for your data type and
300+
-- then define @'typeName' = 'typeNameDefault'@ in its 'Struct' instance.
301+
--
302+
-- This generates a struct name that consists of the name of the original
303+
-- Haskell data type, but converted to snake_case.
304+
typeNameDefault :: (Generic a, GDatatype (Rep a)) => a -> String
305+
typeNameDefault = convert . gTypeName . from
306+
where
307+
convert :: String -> String
308+
convert = convert' True True
309+
310+
convert' :: Bool -- ^ Is this the first letter
311+
-> Bool -- ^ Was the previous letter capital
312+
-> String -- ^ Remainder of the string
313+
-> String
314+
convert' _ _ [] = []
315+
convert' _ v [x]
316+
| v && isUpper x = toLower x : []
317+
| isUpper x = '_' : toLower x : []
318+
| otherwise = x : []
319+
convert' b v (x1:x2:xs)
320+
| not b && isUpper x1 && (isLower x2 || not v)
321+
= '_' : toLower x1 : convert' False (isUpper x1) (x2:xs)
322+
| otherwise
323+
= toLower x1 : convert' False (isUpper x1) (x2:xs)
324+
325+
-- | A default implementation of 'toValues' that leverages 'Generic'. In order
326+
-- to use this, make sure you derive a 'Generic' instance for your data type and
327+
-- then define @'toValues' = 'toValuesDefault'@ in its 'Struct' instance.
328+
toValuesDefault :: (Generic a, GStruct (Rep a)) => a -> [Value a]
329+
toValuesDefault x = coerce $ gToValues $ from x
330+
331+
-- | A default implementation of 'updateField' that leverages 'Generic'. In
332+
-- order to use this, make sure you derive a 'Generic' instance for your data
333+
-- type and then define @'updateField' = 'updateFieldDefault'@ in its 'Struct'
334+
-- instance.
335+
updateFieldDefault :: (Generic a, GStruct (Rep a)) => a -> Value t -> a
336+
updateFieldDefault a v@(Value _ field)
337+
| updated = to a'
338+
| otherwise = error $ "Unexpected field: " ++ show field
339+
where
340+
(a', updated) = gUpdateField (from a) v
341+
342+
-- | A default implementation of 'typeOf' that leverages 'Generic'. In order to
343+
-- use this, make sure you derive a 'Generic' instance for your data type and
344+
-- then define @'typeOf' = 'typeOfDefault'@ in its 'Typed' instance.
345+
typeOfDefault ::
346+
forall a. (Typed a, Struct a, Generic a, GTypedStruct (Rep a)) => Type a
347+
typeOfDefault = Struct $ to $ gStructPlaceholder @(Rep a) @()
348+
349+
-- ** Generic-based classes (not exported)
350+
351+
-- | Capture the name of a Haskell data type from its 'Generic' metadata.
352+
class GDatatype f where
353+
-- | Returns the name of a Haskell data type. (Note that this differs from
354+
-- 'typeName', which is expected to return the name of the struct in the
355+
-- /target/ language).
356+
gTypeName :: f p -> String
357+
358+
-- | The only 'GDatatype' instance we need is for 'D1', which describes
359+
-- 'Datatype' metadata (@d@). We ignore all other metadata (@_f@).
360+
instance Datatype d => GDatatype (D1 d _f) where
361+
gTypeName = datatypeName
362+
363+
-- | Perform struct-related operations on 'Generic' representation types.
364+
class GStruct f where
365+
-- | Transforms all the struct representation's fields into a list of values.
366+
gToValues :: f p -> [Value (f p)]
367+
368+
-- | Update the value of a struct representation's field. This returns two
369+
-- things:
370+
--
371+
-- 1. @f p@: The struct representation, but with the field updated.
372+
--
373+
-- 2. 'Bool': This is 'True' if the field was successfully updated and 'False'
374+
-- otherwise. If this returns 'False', it is the responsibility of the
375+
-- caller to raise an error.
376+
gUpdateField :: f p -> Value t -> (f p, Bool)
377+
378+
-- | 'U1' represents a data constructor with no fields. As such, 'gToValues'
379+
-- returns an empty list of 'Value's, and 'gUpdateField' does not update
380+
-- anything.
381+
instance GStruct U1 where
382+
gToValues U1 = []
383+
gUpdateField u _ = (u, False)
384+
385+
-- | 'M1' is only used to store metadata, which the 'GStruct' class does not
386+
-- make use of. As such, this instance discards the metadata and recurses.
387+
instance GStruct f => GStruct (M1 _i _c f) where
388+
gToValues (M1 x) = coerce (gToValues x)
389+
gUpdateField (M1 x) v = (M1 x', updated)
390+
where
391+
(x', updated) = gUpdateField x v
392+
393+
-- | @(':*:')@ represents a data constructor with multiple fields.
394+
instance (GStruct f, GStruct g) => GStruct (f :*: g) where
395+
-- Recursively compute two lists of Values and append them.
396+
gToValues (f :*: g) = coerce (gToValues f) ++ coerce (gToValues g)
397+
-- Recursively attempt to update the field in both branches and combine the
398+
-- updated branches. We will have successfully updated the field if either
399+
-- branch was successfully updated.
400+
gUpdateField (f :*: g) v = (f' :*: g', updatedF || updatedG)
401+
where
402+
(f', updatedF) = gUpdateField f v
403+
(g', updatedG) = gUpdateField g v
404+
405+
-- | 'K1' represents a single field in a data constructor. This is the base
406+
-- case.
407+
instance (KnownSymbol name, Typed ty, c ~ Field name ty) =>
408+
GStruct (K1 _i c) where
409+
-- Now that we have the field, return it in a singleton list.
410+
gToValues (K1 field) = [Value typeOf field]
411+
-- In order to update the field, we check that the field names and types
412+
-- match. If they do, return the updated field and declare the update as
413+
-- successful. Otherwise, return the old field and declare the update as
414+
-- unsuccessful.
415+
gUpdateField (K1 oldField) (Value newTy (newField :: Field newName newTy)) =
416+
case (sameSymbol pName pNewName, testEquality ty newTy) of
417+
(Just Refl, Just Refl) -> (K1 newField, True)
418+
_ -> (K1 oldField, False)
419+
where
420+
pName = Proxy @name
421+
pNewName = Proxy @newName
422+
ty = typeOf @ty
423+
424+
-- | Compute a 'Generic' placeholder value to use for a struct type.
425+
class GTypedStruct f where
426+
-- | A placeholder value to supply to use in a generic implementation of
427+
-- 'typeOf' for a struct type.
428+
gStructPlaceholder :: f p
429+
430+
-- | 'U1' represents a data constructor with no fields. As such,
431+
-- 'gStructPlaceholder' simply returns the data constructor with no other
432+
-- information.
433+
instance GTypedStruct U1 where
434+
gStructPlaceholder = U1
435+
436+
-- | 'M1' is only used to store metadata, which the 'GTypedStruct' class does
437+
-- not make use of. As such, this instance recursively computes a placeholder
438+
-- value without inspecting the metadata.
439+
instance GTypedStruct f => GTypedStruct (M1 _i _c f) where
440+
gStructPlaceholder = M1 gStructPlaceholder
441+
442+
-- | @(':*:')@ represents a data constructor with multiple fields. As such,
443+
-- 'gStructPlaceholder' recursively computes placeholders for each field and
444+
-- combines them into the overall data constructor.
445+
instance (GTypedStruct f, GTypedStruct g) => GTypedStruct (f :*: g) where
446+
gStructPlaceholder = gStructPlaceholder :*: gStructPlaceholder
447+
448+
-- | 'K1' represents a single field in a data constructor. This is the base
449+
-- case. This instance computes a placeholder value that works for any field of
450+
-- any type.
451+
instance (c ~ Field name ty) => GTypedStruct (K1 _i c) where
452+
-- We use 'undefined' as the actual value for the 'Field' because Copilot
453+
-- never inspects the value.
454+
gStructPlaceholder = K1 $ Field undefined

copilot/CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
2025-01-02
1+
2025-01-06
22
* Bump upper version constraint on filepath. (#570)
3+
* Update struct examples to use generic method implementations. (#564)
34

45
2024-11-07
56
* Version bump (4.1). (#561)

copilot/examples/Structs.hs

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
-- | An example showing how specifications involving structs (in particular,
22
-- nested structs) are compiled to C using copilot-c99.
33

4-
{-# LANGUAGE DataKinds #-}
4+
{-# LANGUAGE DataKinds #-}
5+
{-# LANGUAGE DeriveGeneric #-}
56

67
module Main where
78

89
import qualified Prelude as P
910
import Control.Monad (void, forM_)
11+
import GHC.Generics (Generic)
1012

1113
import Language.Copilot
1214
import Copilot.Compile.C99
1315

1416
-- | Definition for `Volts`.
1517
data Volts = Volts
16-
{ numVolts :: Field "numVolts" Word16
17-
, flag :: Field "flag" Bool
18-
}
18+
{ numVolts :: Field "numVolts" Word16
19+
, flag :: Field "flag" Bool
20+
}
21+
deriving Generic
1922

2023
-- | `Struct` instance for `Volts`.
2124
instance Struct Volts where
22-
typeName _ = "volts"
23-
toValues volts = [ Value Word16 (numVolts volts)
24-
, Value Bool (flag volts)
25-
]
25+
typeName = typeNameDefault
26+
toValues = toValuesDefault
2627
-- Note that we do not implement `updateField` here. `updateField` is only
2728
-- needed to make updates to structs work in the Copilot interpreter, and we
2829
-- do not use the interpreter in this example. (See
@@ -31,29 +32,25 @@ instance Struct Volts where
3132

3233
-- | `Volts` instance for `Typed`.
3334
instance Typed Volts where
34-
typeOf = Struct (Volts (Field 0) (Field False))
35+
typeOf = typeOfDefault
3536

3637
data Battery = Battery
37-
{ temp :: Field "temp" Word16
38-
, volts :: Field "volts" (Array 10 Volts)
39-
, other :: Field "other" (Array 10 (Array 5 Word32))
40-
}
38+
{ temp :: Field "temp" Word16
39+
, volts :: Field "volts" (Array 10 Volts)
40+
, other :: Field "other" (Array 10 (Array 5 Word32))
41+
}
42+
deriving Generic
4143

4244
-- | `Battery` instance for `Struct`.
4345
instance Struct Battery where
44-
typeName _ = "battery"
45-
toValues battery = [ Value typeOf (temp battery)
46-
, Value typeOf (volts battery)
47-
, Value typeOf (other battery)
48-
]
46+
typeName = typeNameDefault
47+
toValues = toValuesDefault
4948
-- Note that we do not implement `updateField` here for the same reasons as in
5049
-- the `Struct Volts` instance above.
5150

52-
-- | `Battery` instance for `Typed`. Note that `undefined` is used as an
53-
-- argument to `Field`. This argument is never used, so `undefined` will never
54-
-- throw an error.
51+
-- | `Battery` instance for `Typed`.
5552
instance Typed Battery where
56-
typeOf = Struct (Battery (Field 0) (Field undefined) (Field undefined))
53+
typeOf = typeOfDefault
5754

5855
spec :: Spec
5956
spec = do

0 commit comments

Comments
 (0)