|
|
`haskell-src-exts` ist von der Benutzung her sehr simpel. Es gibt im Wesentlichen Funktionen, um Haskell-Quellcode zu einem AST zu parsen, Funktionen, um einen (geänderten) AST wieder in entsprechenden Quellcode zu schreiben, und eine Syntaxdefinition, die in Ihrer Handhabung fast genau so einfach ist, wie die Syntaxdefinitionen aus der Vorlesung Übersetzerbau. Der einzige Unterschied ist, dass anders als in dieser Vorlesung die ASTs von den `parse...`-Funktionen durch Positionen ausgezeichnet sind, an denen die entsprechenden Konstrukte im Quellcode gefunden wurden. Diese können genutzt werden, um den Kontext von Fehlermeldungen anzugeben und um den Quellcode in seiner Urform wieder aus dem AST zu gewinnen. Sie sind jedoch nicht nötig. Bei fehlenden Positionsangaben nutzt der Pretty-Printer Standardmöglichkeiten zur Positionierung.
|
|
|
|
|
|
Zu dieser Einführung existiert ein [Hilfsmodul](einfuhrung-helper.hs), in dem einige Funktionen zur einfacheren Benutzung, sowie der Code, um den AST zu modifizieren, bereitgestellt werden. Unter anderem befinden sich darin:
|
|
|
|
|
|
* `parseFile'`, das parseFile benutzt, aber versucht, das Ergebnis als `ParseOk` (also frei von Syntaxfehlern) zu interpretieren
|
|
|
* `removeLoc`, das von allen Elementen eines `haskell-src-exts`-ASTs die Positionsinformationen entfernt
|
|
|
* `mapDecl`, das eine Funktion, die Deklarationen ändert, auf Module hochzieht
|
|
|
|
|
|
Genug Vorgeplänkel, betrachten wir ein paar
|
|
|
|
|
|
## Beispiele
|
|
|
|
|
|
Mit
|
|
|
```{.haskell}
|
|
|
parseFile :: FilePath -> IO (ParseResult (Module SrcSpanInfo))
|
|
|
```
|
|
|
kann man eine Haskell-Quelldatei einlesen. `ParseResult` ist dabei eine Fehlermonade, die bei einem Syntaxfehler die entsprechende Position mitführt. `Module` ist der Typ, der den Syntaxbaum eines ganzen Haskell-Moduls repräsentiert. `SrcSpanInfo` stellt Quellcodepositionen dar.
|
|
|
Im Folgenden werden wir allerdings zugunsten der Lesbarkeit diese Positionen auf `()` projezieren.
|
|
|
|
|
|
#### Minimalbeispiel
|
|
|
|
|
|
Parsen wir die Quelldatei
|
|
|
```{.haskell}
|
|
|
t1 :: Bool
|
|
|
t1 = not 42
|
|
|
```
|
|
|
, so erhalten wir das Ergebnis den AST
|
|
|
```{.haskell}
|
|
|
Module () Nothing [] [] [
|
|
|
TypeSig () [Ident () "t1"] (TyCon () (UnQual () (Ident () "Bool"))),
|
|
|
PatBind ()
|
|
|
(PVar () (Ident () "t1"))
|
|
|
(UnGuardedRhs () (App ()
|
|
|
(Var () (UnQual () (Ident () "not")))
|
|
|
(Lit () (Int () 42 "42"))))
|
|
|
Nothing]
|
|
|
```
|
|
|
. An diesem lässt sich bereits der Zweck vieler Konstruktoren erkennen.
|
|
|
|
|
|
#### `data`
|
|
|
|
|
|
Wir parsen den Quelltext
|
|
|
|
|
|
```{.haskell}
|
|
|
data Foo = Bar | Baz Int | Foobar Foo
|
|
|
|
|
|
magic :: Foo -> Foo
|
|
|
magic Bar = Bar
|
|
|
magic (Baz i) = Baz $ i+1
|
|
|
magic (Foobar f) = Foobar (magic f)
|
|
|
```
|
|
|
|
|
|
Und erhalten den Syntaxbaum:
|
|
|
|
|
|
```{.haskell}
|
|
|
Module () Nothing [] [] [
|
|
|
DataDecl () (DataType ()) Nothing (DHead () (Ident () "Foo")) [
|
|
|
QualConDecl () Nothing Nothing (ConDecl () (Ident () "Bar") []),
|
|
|
QualConDecl () Nothing Nothing (ConDecl () (Ident () "Baz") [
|
|
|
TyCon () (UnQual () (Ident () "Int"))]),
|
|
|
QualConDecl () Nothing Nothing (ConDecl () (Ident () "Foobar") [
|
|
|
TyCon () (UnQual () (Ident () "Foo"))])] Nothing,
|
|
|
TypeSig () [Ident () "magic"] (TyFun ()
|
|
|
(TyCon () (UnQual () (Ident () "Foo")))
|
|
|
(TyCon () (UnQual () (Ident () "Foo")))),
|
|
|
FunBind () [Match ()
|
|
|
(Ident () "magic")
|
|
|
[PApp () (UnQual () (Ident () "Bar")) []]
|
|
|
(UnGuardedRhs () (Con () (UnQual () (Ident () "Bar"))))
|
|
|
Nothing,
|
|
|
Match ()
|
|
|
(Ident () "magic")
|
|
|
[PParen () (PApp () (UnQual () (Ident () "Baz")) [PVar () (Ident () "i")])]
|
|
|
(UnGuardedRhs () (InfixApp ()
|
|
|
(Con () (UnQual () (Ident () "Baz")))
|
|
|
(QVarOp () (UnQual () (Symbol () "$")))
|
|
|
(InfixApp ()
|
|
|
(Var () (UnQual () (Ident () "i")))
|
|
|
(QVarOp () (UnQual () (Symbol () "+")))
|
|
|
(Lit () (Int () 1 "1")))))
|
|
|
Nothing,
|
|
|
Match ()
|
|
|
(Ident () "magic")
|
|
|
[PParen () (PApp () (UnQual () (Ident () "Foobar")) [PVar () (Ident () "f")])]
|
|
|
(UnGuardedRhs ()
|
|
|
(App ()
|
|
|
(Con () (UnQual () (Ident () "Foobar")))
|
|
|
(Paren ()
|
|
|
(App ()
|
|
|
(Var () (UnQual () (Ident () "magic")))
|
|
|
(Var () (UnQual () (Ident () "f")))))))
|
|
|
Nothing]]
|
|
|
```
|
|
|
|
|
|
#### Einfache Modifikation
|
|
|
|
|
|
Einer der wichtigsten Typen des Syntaxbaumes ist wohl `Decl`. Wir wollen uns nun eine Funktion ansehen, die solche Deklarationen leicht modifiziert.
|
|
|
```{.haskell}
|
|
|
renamePatBinds :: Decl l -> Decl l
|
|
|
```
|
|
|
Für dieses einfache Beispiel wollen wir nur beim Binden eines Ausdrucks an eine Variable den Namen dieser gebundenen Variable ändern. Ja, das ist etwas sinnfrei ;)
|
|
|
```{.haskell}
|
|
|
renamePatBinds (PatBind l p r b) = (PatBind l p' r b)
|
|
|
```
|
|
|
Die Codeposition (`l`), den zugewiesenen Ausdruck (`r`) sowie eventuelle zusätzliche Binds (`b`) wollen wir dabei nicht ändern. Ändern wir also die linke Seite (`p`, da dort ein beliebiges Pattern stehen kann).
|
|
|
Wir ändern nur einfache Pattern, die nur aus einer Variable bestehen, indem wir ihren Identifier-String erweitern.
|
|
|
```{.haskell}
|
|
|
where
|
|
|
p' = case p of
|
|
|
(PVar l (Ident l' i)) -> (PVar l (Ident l' ("orig_" ++ i)))
|
|
|
a -> a
|
|
|
```
|
|
|
So fehlt nurnoch die Catchall-Bindung, die alle anderen Deklarationen unverändert übernimmt.
|
|
|
```{.haskell}
|
|
|
renamePatBinds d = d
|
|
|
```
|
|
|
|
|
|
Wenn wir diese Funktion nun auf alle Deklarationen eines Moduls anwenden (z.B. mit `mapDecl` aus dem oben verlinkten Hilfsmodul zu dieser Einführung), erhalten wir aus dem ersten einfachen Beispiel den Syntaxbaum
|
|
|
|
|
|
```{.haskell}
|
|
|
Module () Nothing [] [] [
|
|
|
TypeSig () [Ident () "t1"] (TyCon () (UnQual () (Ident () "Bool"))),
|
|
|
PatBind ()
|
|
|
(PVar () (Ident () "orig_t1"))
|
|
|
(UnGuardedRhs () (App () (Var () (UnQual () (Ident () "not"))) (Lit () (Int () 42 "42"))))
|
|
|
Nothing]
|
|
|
```
|
|
|
|
|
|
Rufen wir `prettyPrint` auf diesen Syntaxbaum auf, so erhalten wir wieder Quellcode, aber mit unserer Änderung:
|
|
|
```{.haskell}
|
|
|
t1 :: Bool
|
|
|
orig_t1 = not 42
|
|
|
```
|
|
|
Offensichtlich fehlt noch das Umbenennen in der Signatur. Die Implementierung dieser Funktion sei dem Leser als Übung überlassen.
|
|
|
|
|
|
#### Etwas aufwändigere Modifikation
|
|
|
|
|
|
Als etwas sinnvolleres Beispiel wollen wir uns eine Funktion anschauen, die all solche Variablen findet, an die ein `checkExpect`-Ausdruck zugewiesen wird.
|
|
|
```{.haskell}
|
|
|
pullTest :: Decl l -> Maybe (Name l)
|
|
|
```
|
|
|
Zunächst matchen wir nur die einfache Applikation und die Applikation mit `$`.
|
|
|
```{.haskell}
|
|
|
pullTest (PatBind _ (PVar _ a@(Ident _ _)) (UnGuardedRhs _ (App _ (App _ (Var _ (UnQual _ (Ident _ "checkExpect"))) _) _)) _) = Just a
|
|
|
pullTest (PatBind _ (PVar _ a@(Ident _ _)) (UnGuardedRhs _ (InfixApp _ (App _ (Var _ (UnQual _ (Ident _ "checkExpect"))) _) (QVarOp _ (UnQual _ (Symbol _ "$"))) _)) _) = Just a
|
|
|
```
|
|
|
Es bleiben die Fälle, die keinen `checkExpect`-Ausdruck binden. Diese wollen wir nicht aufsammeln.
|
|
|
```{.haskell}
|
|
|
pullTest _ = Nothing
|
|
|
```
|
|
|
|
|
|
Schließlich muss die Funktion nur noch auf ein ganzes Modul hochgezogen werden, das erledigen die folgenden Funktionen kommentarlos:
|
|
|
```{.haskell}
|
|
|
pullTests :: [Decl l] -> [Name l]
|
|
|
pullTests = catMaybes . map pullTest
|
|
|
|
|
|
pullTestsFromModule :: Module l -> [Name l]
|
|
|
pullTestsFromModule (Module _ _ _ _ ds) = pullTests ds
|
|
|
```
|
|
|
|
|
|
Als Testmodul nehmen wir dieses hier:
|
|
|
```{.haskell}
|
|
|
t1 = checkExpect 3 $ add 2 1
|
|
|
t2 = checkExpect 0 $ add 1 (-1)
|
|
|
t3 = checkExpect 0 $ add 0 0
|
|
|
t4 = checkExpect 100042 $ add 42 100000
|
|
|
t5 = checkExpect 10 (add 5 5)
|
|
|
add :: Int -> Int -> Int
|
|
|
add = (+)
|
|
|
```
|
|
|
|
|
|
So erhalten wir die Ausgabe
|
|
|
```{.haskell}
|
|
|
*Main> removeLoc <$$> pullTestsFromModule <$> parseFile' "tests.hs"
|
|
|
[Ident () "t1",Ident () "t2",Ident () "t3",Ident () "t4",Ident () "t5"]
|
|
|
```
|
|
|
Wir haben also alle checkExpect-Ausdrücke gefunden. |
|
|
\ No newline at end of file |