Skip to content

Commit fcd2885

Browse files
authored
Rewrite rules: specifiable imports from Prelude (#193)
* Now the handling of Prelude can be specified in the config file * Updating documentation
1 parent 4ab9b82 commit fcd2885

File tree

8 files changed

+264
-133
lines changed

8 files changed

+264
-133
lines changed

docs/source/features.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,9 @@ testA = A.foo
891891
testB = A.foo
892892
```
893893

894-
# Rewrite rules
894+
# Rewrite rules and Prelude imports
895+
896+
## Rewrite rules
895897

896898
User-defined rewrite rules for names can be defined through YAML configuration files. These should be provided via the `--rewrite-rules` option.
897899

@@ -1008,8 +1010,38 @@ denominatorMinus1 = fromIntegral . denominator
10081010

10091011
See also `rewrite-rules-example.yaml` in the root of the repository.
10101012

1011-
**Known issues:**
1012-
- If you import something from Prelude, you have to state this explicitly.
1013-
- Then, it will add an import like import Prelude (...), which shadows all the other functions in Prelude. This is not necessarily a problem; as we will probably not modify the generated Haskell files by hand, and so we won't add any new Prelude functions.
1014-
- But this only works for things you do use, not for those that you only define. For example, if you write an instance of the Num class and define signum but do not use it, it will not get into Prelude's import list, and so GHC will complain.
1013+
## Handling of Prelude
1014+
1015+
By default, agda2hs handles Prelude like other modules: it collects all the identifiers it finds we use from Prelude, and adds them to Prelude's import list.
1016+
1017+
In the config YAML, we can specify a different behaviour. The format is:
1018+
1019+
```yaml
1020+
# First, we specify how to handle Prelude.
1021+
prelude:
1022+
implicit: true
1023+
hiding: # if implicit is true
1024+
- seq
1025+
1026+
#using: # if implicit is false
1027+
# - +
1028+
# - Num
1029+
1030+
# Then the rules themselves.
1031+
rules:
1032+
1033+
# The rational type.
1034+
- from: Data.Rational.Unnormalised.Base.ℚᵘ
1035+
to: Rational
1036+
importing: Data.Ratio
1037+
- [...]
1038+
```
1039+
1040+
If `implicit` is `true`, then everything gets imported from Prelude, except for those that are specified in the `hiding` list. This can cause clashes if you reuse names from Prelude, hence the opportunity for a `hiding` list. If there is no such list, then everything gets imported.
1041+
1042+
If `implicit` is `false`, Prelude gets imported explicitly, and only those identifiers that are specified in the `using` list. If there is no such list, agda2hs reverts to the default behaviour (it tries to collect imports by itself).
1043+
1044+
## Known issues
1045+
1046+
- Rewrite rules only work for things you do use, not for those that you only define. This causes a problem with class instances: if you choose the default behaviour, then write an instance of the Num class and define signum but do not use it, it will not get into Prelude's import list, and so GHC will complain.
10151047
- You cannot change to a version with arguments of different modality without getting useless code. So if you rewrite a function to a version which has some of its parameters erased, the parameters remain there; probably because rewriting happens only after compiling the type signature.

rewrite-rules-example.yaml

Lines changed: 95 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,104 @@
11
# An example config file for the '--rewrite-rules' option.
22
# Imagine a library which uses the Agda standard library extensively and let's suppose we would like to translate its operators to the Haskell equivalents. The library includes a Haskell.Prim.Num.Num instance.
33

4-
# We have to include this so that Num is written to the import list of Prelude.
5-
- from: Haskell.Prim.Num.Num
6-
to: Num
7-
importing: Prelude
8-
# And it doesn't see these by default either, if not included in Prelude's import list.
9-
- from: Haskell.Prim.Num.Num.fromInteger
10-
to: fromInteger
11-
importing: Prelude
12-
- from: Haskell.Prim.Num.Num.signum
13-
to: signum
14-
importing: Prelude
4+
# First, we specify how to handle Prelude.
5+
prelude:
6+
implicit: true
7+
hiding: # if implicit is true
8+
- seq
159

16-
# The rational type.
17-
- from: Data.Rational.Unnormalised.Base.ℚᵘ
18-
to: Rational
19-
importing: Data.Ratio
10+
#using: # if implicit is false
11+
# - + # (+) should be given like that
12+
# - Num
2013

21-
# Arithmetic operators.
22-
# Note: Prelude has to be specified too!
23-
- from: Agda.Builtin.Nat._+_
24-
to: _+_
25-
importing: Prelude
26-
- from: Agda.Builtin.Nat._*_
27-
to: _*_
28-
importing: Prelude
29-
- from: Agda.Builtin.Nat.-_
30-
to: negate
31-
importing: Prelude
32-
- from: Agda.Builtin.Nat._-_
33-
to: _-_
34-
importing: Prelude
35-
- from: Data.Nat.Base._⊔_
36-
to: max
37-
importing: Prelude
14+
#Then the rules themselves.
15+
rules:
3816

39-
- from: Agda.Builtin.Integer._+_
40-
to: _+_
41-
importing: Prelude
42-
- from: Data.Integer.Base._*_
43-
to: _*_
44-
importing: Prelude
45-
- from: Agda.Builtin.Integer.-_
46-
to: negate
47-
importing: Prelude
48-
- from: Agda.Builtin.Integer._-_
49-
to: _-_
50-
importing: Prelude
17+
# The rational type.
18+
- from: Data.Rational.Unnormalised.Base.ℚᵘ
19+
to: Rational
20+
importing: Data.Ratio
5121

52-
- from: Data.Rational.Unnormalised.Base._+_
53-
to: _+_
54-
importing: Prelude
55-
- from: Data.Rational.Unnormalised.Base._*_
56-
to: _*_
57-
importing: Prelude
58-
- from: Data.Rational.Unnormalised.Base.-_
59-
to: negate
60-
importing: Prelude
61-
- from: Data.Rational.Unnormalised.Base._-_
62-
to: _-_
63-
importing: Prelude
64-
- from: Data.Rational.Unnormalised.Base._/_
65-
to: _%_
66-
importing: Data.Ratio
67-
- from: Data.Rational.Unnormalised.Base._⊔_
68-
to: max
69-
importing: Prelude
70-
- from: Data.Rational.Unnormalised.Base._⊓_
71-
to: min
72-
importing: Prelude
73-
- from: Data.Rational.Unnormalised.Base.∣_∣
74-
to: abs
75-
importing: Prelude
76-
- from: Data.Integer.Base.∣_∣
77-
to: intAbs
78-
importing: Base
22+
# Arithmetic operators.
23+
# Note: Prelude has to be specified too!
24+
- from: Agda.Builtin.Nat._+_
25+
to: _+_
26+
importing: Prelude
27+
- from: Agda.Builtin.Nat._*_
28+
to: _*_
29+
importing: Prelude
30+
- from: Agda.Builtin.Nat.-_
31+
to: negate
32+
importing: Prelude
33+
- from: Agda.Builtin.Nat._-_
34+
to: _-_
35+
importing: Prelude
36+
- from: Data.Nat.Base._⊔_
37+
to: max
38+
importing: Prelude
7939

80-
# Standard library functions related to naturals and integers.
81-
- from: Agda.Builtin.Nat.Nat.suc
82-
to: suc
83-
importing: Base
84-
- from: Agda.Builtin.Int.Int.pos
85-
to: toInteger
86-
importing: Prelude
87-
- from: Data.Integer.DivMod._divℕ_
88-
to: divNat
89-
importing: Base
90-
- from: Data.Nat.DivMod._/_
91-
to: div
92-
importing: Prelude
40+
- from: Agda.Builtin.Integer._+_
41+
to: _+_
42+
importing: Prelude
43+
- from: Data.Integer.Base._*_
44+
to: _*_
45+
importing: Prelude
46+
- from: Agda.Builtin.Integer.-_
47+
to: negate
48+
importing: Prelude
49+
- from: Agda.Builtin.Integer._-_
50+
to: _-_
51+
importing: Prelude
9352

94-
# Standard library functions related to rationals.
95-
- from: Data.Rational.Unnormalised.Base.ℚᵘ.numerator
96-
to: numerator
97-
importing: Data.Ratio
98-
- from: Data.Rational.Unnormalised.Base.ℚᵘ.denominator
99-
to: denominatorNat
100-
importing: Base #the Base version returns a Natural
101-
- from: Data.Rational.Unnormalised.Base.ℚᵘ.denominator-1
102-
to: denominatorMinus1
103-
importing: Base
53+
- from: Data.Rational.Unnormalised.Base._+_
54+
to: _+_
55+
importing: Prelude
56+
- from: Data.Rational.Unnormalised.Base._*_
57+
to: _*_
58+
importing: Prelude
59+
- from: Data.Rational.Unnormalised.Base.-_
60+
to: negate
61+
importing: Prelude
62+
- from: Data.Rational.Unnormalised.Base._-_
63+
to: _-_
64+
importing: Prelude
65+
- from: Data.Rational.Unnormalised.Base._/_
66+
to: _%_
67+
importing: Data.Ratio
68+
- from: Data.Rational.Unnormalised.Base._⊔_
69+
to: max
70+
importing: Prelude
71+
- from: Data.Rational.Unnormalised.Base._⊓_
72+
to: min
73+
importing: Prelude
74+
- from: Data.Rational.Unnormalised.Base.∣_∣
75+
to: abs
76+
importing: Prelude
77+
- from: Data.Integer.Base.∣_∣
78+
to: intAbs
79+
importing: Base
80+
81+
# Standard library functions related to naturals and integers.
82+
- from: Agda.Builtin.Nat.Nat.suc
83+
to: suc
84+
importing: Base
85+
- from: Agda.Builtin.Int.Int.pos
86+
to: toInteger
87+
importing: Prelude
88+
- from: Data.Integer.DivMod._divℕ_
89+
to: divNat
90+
importing: Base
91+
- from: Data.Nat.DivMod._/_
92+
to: div
93+
importing: Prelude
94+
95+
# Standard library functions related to rationals.
96+
- from: Data.Rational.Unnormalised.Base.ℚᵘ.numerator
97+
to: numerator
98+
importing: Data.Ratio
99+
- from: Data.Rational.Unnormalised.Base.ℚᵘ.denominator
100+
to: denominatorNat
101+
importing: Base #the Base version returns a Natural
102+
- from: Data.Rational.Unnormalised.Base.ℚᵘ.denominator-1
103+
to: denominatorMinus1
104+
importing: Base

src/Agda2Hs/Compile.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ runC tlm rewrites = runWriterT
4848

4949
compile :: Options -> ModuleEnv -> IsMain -> Definition ->
5050
TCM (CompiledDef, CompileOutput)
51-
compile opts tlm _ def = withCurrentModule (qnameModule $ defName def) $ runC tlm (rewriteRules opts) $
51+
compile opts tlm _ def = withCurrentModule (qnameModule $ defName def) $ runC tlm (optRewrites opts) $
5252
compileAndTag <* postCompile
5353
where
5454
tag code = [(nameBindingSite $ qnameName $ defName def, code)]

src/Agda2Hs/Compile/Imports.hs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
module Agda2Hs.Compile.Imports ( compileImports ) where
2+
module Agda2Hs.Compile.Imports ( compileImports, makeManualDecl ) where
33

44
import Data.Char ( isUpper )
55
import Data.List ( isPrefixOf )
@@ -60,7 +60,9 @@ compileImports top is0 = do
6060
makeImportDecl :: Hs.ModuleName () -> Qualifier -> ImportSpecMap -> Hs.ImportDecl ()
6161
makeImportDecl mod qual specs = Hs.ImportDecl ()
6262
mod (isQualified qual) False False Nothing (qualifiedAs qual)
63-
(Just $ Hs.ImportSpecList () False $ map (uncurry makeImportSpec) $ Map.toList specs)
63+
(Just $ Hs.ImportSpecList ()
64+
False -- whether the list should be a list of hidden identifiers ('hiding')
65+
$ map (uncurry makeImportSpec) $ Map.toList specs)
6466

6567
checkClashingImports :: Imports -> TCM ()
6668
checkClashingImports [] = return ()
@@ -76,3 +78,12 @@ compileImports top is0 = do
7678
++ prettyShow (pp <$> p') ++ ")"
7779
-- TODO: no range information as we only have Haskell names at this point
7880

81+
-- if the user has provided a "using" or "hiding" list in the config file
82+
-- used for Prelude
83+
makeManualDecl :: Hs.ModuleName () -> Qualifier -> Bool -> [String] -> Hs.ImportDecl ()
84+
makeManualDecl mod qual isImplicit namesToHide = Hs.ImportDecl ()
85+
mod (isQualified qual) False False Nothing (qualifiedAs qual)
86+
(Just $ Hs.ImportSpecList ()
87+
isImplicit -- whether the list should be a list of hidden identifiers ('hiding')
88+
$ map (Hs.IVar() . (\str -> if validVarId str || validConId str then Hs.Ident() str else Hs.Symbol() str)) -- since we can only read strings from the config file, we have to do this
89+
namesToHide) -- map (uncurry makeImportSpec) $ Map.toList specs)

src/Agda2Hs/Compile/Rewrites.hs

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,73 @@
11
-- Reads custom rewrite rules of the user from a YAML config file.
2-
module Agda2Hs.Compile.Rewrites where
2+
module Agda2Hs.Compile.Rewrites (readConfigFile) where
33

44
import Data.Yaml.YamlLight
55
import qualified Data.Map as Map
66
import qualified Data.Text as Text
77
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
88
import qualified Data.ByteString as ByteString
9+
import Data.Char (toLower)
10+
import Data.Maybe (isJust)
911

10-
-- There is already a RewriteRule identifier in Agda internals, hence the name.
11-
-- Elements:
12-
-- the identifier to rewrite ("from")
13-
-- the identifier with which we replace it ("to")
14-
-- the import to use, if any ("importing")
15-
data Rewrite = Rewrite {from :: String, to :: String, importing :: Maybe String}
12+
import Agda2Hs.Compile.Types
1613

17-
type Rewrites = [Rewrite]
14+
-- Conversions from String to ByteString and vice versa.
15+
-- We assume UTF-8.
16+
stringToByteString :: String -> ByteString.ByteString
17+
stringToByteString = encodeUtf8 . Text.pack
18+
byteStringToString :: ByteString.ByteString -> String
19+
byteStringToString = Text.unpack . decodeUtf8
20+
21+
-- A simple element consisting of the string given. Useful when looking up maps.
22+
stringElement :: String -> YamlLight
23+
stringElement = YStr . stringToByteString
24+
25+
-- Read Prelude options from a YamlLight object. Fail if the format is incorrect.
26+
preludeOptionsFromYaml :: YamlLight -> Maybe PreludeOptions
27+
preludeOptionsFromYaml yaml = case lookupYL (stringElement "prelude") yaml of
28+
Nothing -> Nothing
29+
Just (YMap dmap) -> Just $ preludeOptionsFromMap dmap
30+
Just otherYaml -> error $ "Incorrect format for the rewrite rules file: the \"prelude\" element must be a map – " ++ show otherYaml
31+
where
32+
preludeOptionsFromMap :: Map.Map YamlLight YamlLight -> PreludeOptions
33+
preludeOptionsFromMap dmap = (impl, namesToImp)
34+
where
35+
impl :: Bool
36+
impl = case Map.lookup (stringElement "implicit") dmap of
37+
Nothing -> isJust $ Map.lookup (YStr $ stringToByteString "hiding") dmap
38+
Just (YStr bs) -> let str = map toLower $ byteStringToString bs in
39+
str == "true" || str == "yes"
40+
Just otherYaml -> error "Incorrect format for the rewrite rules file: \"implicit\" must be a boolean"
1841

19-
-- Read rewrite rules from a given file. Fail if the format is incorrect (necessary fields are absent).
20-
readRewritesFromFile :: FilePath -> IO Rewrites
21-
readRewritesFromFile fname = rulesFromYaml <$> parseYamlFile fname
42+
namesToImp :: NamesToImport
43+
namesToImp = if impl then hidingNames else importingNames -- we search for different keys
44+
where
45+
hidingNames :: NamesToImport
46+
hidingNames = case Map.lookup (stringElement "hiding") dmap of
47+
Nothing -> Names [] -- then we import the whole Prelude
48+
Just (YSeq ls) -> Names $ map (parseElement "hiding") $ ls
49+
Just otherYaml -> error "Incorrect format for the rewrite rules file: \"hiding\" must be a sequence"
50+
51+
importingNames :: NamesToImport
52+
importingNames = case Map.lookup (stringElement "using") dmap of
53+
Nothing -> Auto -- then we let agda2hs search for them
54+
Just (YSeq ls) -> Names $ map (parseElement "using") $ ls
55+
Just otherYaml -> error "Incorrect format for the rewrite rules file: \"\" must be a sequence"
56+
57+
parseElement :: String -> YamlLight -> String
58+
parseElement seqName y = case (unStr y) of
59+
Nothing -> error "Incorrect format for the rewrite rules file: an element in \"" ++ seqName ++ "\" was not a string"
60+
Just bs -> byteStringToString bs
2261

2362
-- Read rewrite rules from a YamlLight object. Fail if the format is incorrect.
2463
rulesFromYaml :: YamlLight -> Rewrites
25-
rulesFromYaml y = case y of
26-
YNil -> []
27-
YSeq ls -> map ruleFromElement ls
28-
otherYaml -> error $ "Incorrect format for the rewrite rules file: must be a list of elements – " ++ show otherYaml
64+
rulesFromYaml y = case lookupYL (stringElement "rules") y of
65+
Nothing -> case y of -- maybe the root is the sequence
66+
YSeq ls -> map ruleFromElement ls
67+
_ -> []
68+
Just YNil -> []
69+
Just (YSeq ls) -> map ruleFromElement ls
70+
Just otherYaml -> error $ "Incorrect format for the rewrite rules file: \"rules\" must be a list of elements – " ++ show otherYaml
2971
where
3072
-- parsing a single element
3173
ruleFromElement :: YamlLight -> Rewrite
@@ -55,9 +97,11 @@ rulesFromYaml y = case y of
5597
Just bytestr -> Right $ byteStringToString $ bytestr
5698
Nothing -> Left $ "Not a string element: " ++ show yaml
5799

58-
-- Conversions from String to ByteString and vice versa.
59-
-- We assume UTF-8.
60-
stringToByteString :: String -> ByteString.ByteString
61-
stringToByteString = encodeUtf8 . Text.pack
62-
byteStringToString :: ByteString.ByteString -> String
63-
byteStringToString = Text.unpack . decodeUtf8
100+
-- The exported function to be used in Main.hs.
101+
readConfigFile :: FilePath -> IO Config
102+
readConfigFile fname = (,) <$>
103+
(preludeOptionsFromYaml <$> wholeYaml) <*>
104+
(rulesFromYaml <$> wholeYaml)
105+
where
106+
wholeYaml :: IO YamlLight
107+
wholeYaml = parseYamlFile fname

0 commit comments

Comments
 (0)