 Okay, so bei mir ist jetzt der Marius von Carz Marchule, der will uns was über beweisbare Software und Programmcodes erzählen und ich bin gespannt Marius, die Bühne ist deine. Vielen Dank. Ich rede heute über Beweise in Software und das eigentlich ein altes Ding. Also Beweise in Software wurden gemacht seit es irgendwie Programmiersprachen gibt und manche von Systemen, die ich jetzt irgendwie heute zeige, wenn auch vielleicht nicht die spannendsten, sind einfach noch aus den 80er Jahren und dieses Thema hat einfach bisher, würde ich sagen, so eine wissenschaftliche Community irgendwie interessiert und ein paar Leute am Rande, aber so richtig irgendwie in den Mainstreamen der Informatik ist es nie gekommen. Das ändert sich aber gerade ein bisschen, ist vielleicht ein bisschen vergleichbar mit irgendwie dem KI-Thema, wo plötzlich irgendwie, wo Rechenleistung da ist, man Sachen automatisieren kann und das ist plötzlich Sachen irgendwie einfacher gehen, weil man es nicht mehr selbst machen oder vielleicht noch nicht mehr selbst ganz verstehen muss. So ist es bei Softwarebeweisen auch, würde ich sagen. Es gibt mittlerweile irgendwie ganz gute Möglichkeiten, wenigstens Teile des Beweisverfahrens auf Software zu automatisieren und es gibt Bestrebungen, die Beweissoftware, die Theorembeweise, nicht mehr nur als ein separates System dazu haben, was irgendwie mit meinem eigentlichen Programm gar nichts zu tun hat, sondern es gibt Bestrebungen Beweise und Beweisbarkeit und irgendwie eine höhere Korrektheit auch in Mainstreamen sprachen, reinzubringen und somit irgendwie mehr Leuten verfügbar zu machen. Und ein paar von diesen, wie ich finde, spannenden Systemen, die irgendwo auf der Grenze zwischen ich beweise etwas selbst und ich lasse irgendwie meine Maschine etwas beweisen, über die wird es jetzt auch gehen teilweise. Wieso das Ganze? Meistens, wenn man irgendwie Vorträge über Softwarekorrektheit hat, kommt jetzt hier irgendwie ein konkretes Beispiel, irgendwie häufig hier diese Ariane-5-Rakete, die ein Softwareback hatte und dann umgedreht ist und ich würde aber eigentlich eher argumentieren wollen mit der Art und Weise, wie wir Software verwenden. Also wenn heutzutage irgendwie mein privates Website backend mit ein paar Umfragen für meine Freunde irgendwie die kritischste Software wäre, dann würde ich sagen, bräuchten wir auch keine Beweisbarkeit in Software. Das ging teilweise in der Fall, ein Software kommt in alle möglichen Bereiche rein, in Dinge, wo man nie dachte, dass vielleicht IT auch mal eine Rolle spielen sollte oder könnte. Ich denke in diesem Umfeld vielleicht noch ein bisschen mehr. Und was auf jeden Fall irgendwie ganz spannend ist, sich auf Wikipedia mal die Liste auf Softwarebacks anzugucken, ist einfach eine Liste an schwerwiegenden Fehlern in Software, die zu schwerwiegenden Problemen in der realen Welt geführt haben. Und wenn man sich jetzt mal überhaupt irgendwie hier die einzelnen Kategorien da anguckt, in denen Software Fehler zu Geldverlust oder teilweise aber auch irgendwie zum Schaden von Menschen geführt haben, da sind Sachen wie Raumfahrt dabei, Medizin, Dinge, die irgendwie unser IT-System insgesamt angreifen, irgendwie so etwas wie Verschüsselung. Und das sind alles irgendwie Bereiche, in denen ich gerne möchte, dass die Software, die ich verwende, dass sie auf der einen Seite transparent ist natürlich, als irgendwie Open Source und dass sie irgendwie gereviewt ist und dass sie getestet ist vielleicht, wo ich aber auch möchte, dass meine Software korrekt arbeitet und ich ganz gerne irgendwie eine Vergewisserung hätte, dass sie das tut. Und hier kommen Softwarebeweise mit ins Spiel. Ich möchte mit zwei Projekten, die gerade irgendwie im Entstehen sind, möchte ich aufzeigen, dass das Ganze kein kleines Gespinde von ein paar Randwissenschaftlern mehr ist, sondern dass das wirklich relevante Dinge sind, die da passieren. Das eine ist das DeepSpec Projekt, das ist ein übergreifendes Projekt von vielen Unis und auch einzelnen Leuten. Und da geht es darum, ein verifiziertes System, also lokal, zum Beispiel, was irgendwie auf einem Server läuft oder was auf meinem Laptop läuft zu kreieren. Das beginnt ganz unten bei der Hardware-Ebene, wo wir Beweise irgendwie auf Hardware-Seite haben, wo wir irgendwie Hardware-Beschreibungssprachen haben und darüber Beweise führen können, dass mein konkretes Platinedesign und meine konkrete Hardware-Implementierung von bestimmten Primitiven genau das macht, was ich auch machen soll. Das geht dann weiter nach oben auf einer Ebene von irgendwelchen Zwischenrepräsentationen wie LLVM oder dann noch ein bisschen höher irgendwie die C-Ebene bis hin zu ganzen Betriebssystemen. Das ist hier auf der linken Seite dieses Zertikos-Projekt, was ein wissenschaftliches Projekt ist und ich weiß auch gar nicht genau, wie anwendbar das ist, aber das war auf jeden Fall das erste Multicore-Betriebssystem, der erste Multicore-Kern, der bewiesen war. Genau, dann geht es hier hoch bis irgendwie in die Ebenen von Hochsprachen wie Haskell, Theorienbeweisern wie Coct, da wird es dann nochmal dazu gekommen, aber auch so Dingen wie irgendwie Datenbanken, SQL und so weiter. Dieses Projekt hat insgesamt das Ziel, dass wir sozusagen auf unserer lokalen Maschine etwas haben, was wir laufen lassen können und wo wir sicher sein können, von ganz oben nach ganz unten in dem gesamten Stack, dass wir irgendwie einen verifizierten Stack haben. Um die Kommunikation zwischen Rechnern geht es bei dem Project Everest. Das ist ein gemeinschaftliches Projekt dieser in-real Universitäten in Frankreich und Microsoft Research und die versuchen einen komplett verifizierten HTT P2 Stack zu programmieren und zu beweisen. Das beginnt unten bei ganz kleinen Krypto-Algorithmen wie Elliptic Curves und RSA und wieder deren Teilprimitiven und das ist auch auf jeden Fall schon irgendwie in einem Status, in dem man das benutzen kann. Das ist nämlich bereits im Linux Kern, als Teil von diesem WireGuard-Protokoll oder als Teil dieses WireGuard-Projektes im Linux Kern übernommen worden und auch im Firefox sind diese verifizierten Kryptoprimitive drin und insbesondere dahingehend spannend, finde ich, weil das alles generierter C-Code im Endeffekt ist. Das heißt, es wird in einer höheren Sprache, in diesem Fall F-Star, da wird es dann auch nochmal später drum gehen, wird eine Software geschrieben, werden diese Primitive geschrieben, werden diese Primitive gleich noch bewiesen und dann wird daraus generiert und die Leute vom Project Everest haben es geschafft, dass dieser generierte C-Code so lesbar und so verständlich ist, in einem langen Prozess gibt es einen spannenden Vortrag zu, sodass das durch die Qualitätsanalyse gekommen ist, sowohl bei Firefox, bei Mozilla, wie auch einfach in den Linux Kern dann. Und Leute gesagt haben, okay, das ist das Code, der es zwar generiert, aber der ist so verständlich, dass wir den übernehmen wollen. Und das ist immer noch ein Projekt, was läuft, aber am Ende steht irgendwie dieses Ziel, dass man einen kompletten HTTP2 Stack mit allen Teilen, mit TLS 1.3 und allem hat. Und ich würde sagen, diese beiden Projekte zeigen schon, dass wir in diesem Bereich nicht mehr irgendwie an kleine Software-Dinge denken, sondern dass es die Möglichkeit gibt, zumindest, dass wir den ganzen Software-Ebenen austauschen durch verifizierte Software. Hier sehen wir auf der rechten Seite auch schon drei Systeme, die wir dann noch zumindest kurz besprechen werden. Das Z3 fällt da ein bisschen raus. Das ist ein SMT-Säufer, das heißt irgendwie ein Logik-Säufer, der den kann man füttern mit logischen Aussagen und der wird einem hinterher eine variable Belegung zum Beispiel herausgeben, um diese logischen Aussagen wahrzumachen. Und diese SMT-Säufer sind auf jeden Fall ein Fundament von dieser neuen Welle von Programmiersprachen, die teilweise Sachen automatisieren und die Beweise, die ich sonst per Hand führen müsste, an so einem SMT-Säufer übergeben. Und dieses Z3, das hört man auf jeden Fall immer wieder. Es gibt eine Reihe von bekannten SMT-Säufern, aber das ist das, was von den meisten Projekten im Tarners-Standard benutzt wird. Wie macht man das jetzt nun? Wie ist das mit den Beweisen und was hat das eigentlich mit Programmierungen zu tun? Und das ist schon lange bekannt, dass Beweise und Programme eine Parallelität bilden. Das nennt sich Curry Howard Isomorphismus, die aufgezeigt haben, dass es eine Parallelität gibt zwischen logischen Formeln, Spezifikationen, die ich finde, also sozusagen Dinge, die ich beweisen will über meine Software oder vielleicht auch über eine einzelne Funktion und Typen im Typsystem. Und das ist dann, wenn ich sozusagen in meinen Typen irgendwie diese logischen Spezifikationen habe, dass es dann wiederum, dass meine Beweise zu Programm werden können. Das heißt, ich kann eine Programmiersprach verwenden und kann in meinen Typen eine Spezifikation, was jetzt meine Funktion genau tun soll, Vorbedingungen, Nachbedingungen, irgendwelche Seitenbedingungen definieren und kann dann in dem Programm, was ich da schreibe, kann ich aufzeigen, dass diese logischen Verknüpfungen, dass die wahr sind. Wie genau das funktioniert, werden wir gleich noch sehen. Aber die Idee ist so ein bisschen, dass wenn ich jetzt eine normale Standardfunktion in irgendeiner Programmiersprache habe und die hat irgendwie eine Funktionsdefinition, die hat ein Parameter, was vielleicht ein Int und ein String ist, dann ist das Programm, das ich da drunterschreibe, also die Funktion ist eine Art Beweis dafür, dass es eine Funktion geben kann, die mit einem Int und einem String vielleicht einen weiteren Int zurückgibt. Und das bringt mir natürlich erst mal noch nicht viel außer dieser Gedanke, dass das schon so eine Art Beweis ist. Und wenn ich aber jetzt meinen Typen, meinen Typ, der über der Funktion steht, wenn ich den sehr viel detaillierter mache, dann muss ich auch mein Programm ändern, sodass es sehr viel detaillierter aufzeigt, dass es genau das tut, was ich definiert habe in den Typen. Und nicht alle Theorienbeweiser oder nicht alle Beweissysteme nehmen das Typsystem als Basis, aber diese Parallelität zeigt, dass es geht und die meisten machen das auch. Das heißt, ich kann sozusagen in meinen Typen, kann ich sehr viel spezifischer werden und kann konkrete Invarianten aufzeigen, die ich dann im Code aber mit bedenken muss und mit umformen muss, um dem Typsystem, dem erweiterten Typsystem zu zeigen, dass mein Programm wirklich das tut, was ich möchte. Genau, hier sieht man so ein paar Parallelen, vielleicht irgendwie interessant, weil wir es später noch mal haben, ist das im logischen False ist der Bottom Type, das kennt man vielleicht irgendwie, wenn man Haskell schon mal gemacht hat oder sowas, das ist ein Typ, der keine Werte hat. Also während ein Int die Werte 0, 1, 2, 3, 4, 5 hat, hat der Bottom Type einfach keine Werte und das kommt so ein bisschen aus dem Gedanken konstruktiver Logik und dass wir, wenn wir irgendwo ein False haben, dass wir daraus nichts konstruieren können, weil wir keine Werte hinter diesem Typ haben. Dann werden wir noch mal sehen, wie das bei einem konkreten Beispiel aussieht. Genau, dann haben wir irgendwie hier sowas wie Quantifizierung, was man vielleicht schon mal gehört hat, wenn man irgendwo Logik in der Uni hatte oder sowas und da sehen wir auf der rechten Seite diese Product und Some Types und das sind Teile von einer Erweiterung von Typsystemen, die sich dependent Types nennen und die ebenfalls in vielen Beweissystemen anwendung finden. Gucken wir uns mal, welche sprachend System es irgendwie in diesem Bereich gibt und wie man sie einordnen kann. Gucken wir erstmal die Einordnung. Das eine ist eine Unterscheidung zwischen interaktiv und automatisiert. Interaktiv heißt ich beweise etwas. Das ist irgendwie sowas equivalent zu, ich nehme mir ein Zettel und mache einen mathematischen Beweiss Schritt für Schritt, nur dass ich diesen Beweis nicht auf einem Zettel mache, sondern dass ich den in dem Computerprogramm mache und dass mein Computerprogramm hinterher mir auch noch sagen kann, der Beweis, den ich da aufgeschrieben habe, zusammen mit meiner Spezifikation, also dem, was ich beweisen möchte und dem eigentlichen Programm ergibt einen schlüssigen Beweis. Das habe ich jetzt bei so einem Zettel nicht. Da kann ich einen Beweis aufschreiben und kann dann irgendwie viele Mathematiker und Logiker fragen, ist das jetzt eigentlich richtig? In dem Fall kann mir sozusagen meinen Theorembeweiser, meinen System, kann mir sagen, ob ich korrekt lag mit meinem Beweis oder nicht. Automatisiert ist genau das Gegenteil. Da sage ich eben so einem SMT-Solver zum Beispiel, bitte versuch das mal für mich zu lösen und wenn ich Glück habe oder vielleicht viel Zeit mitbringe, je nachdem, was es für ein Beweis ist, dann kann das durchaus sein, dass ich gar nicht selbst Hand anlegen muss. Und davon hört man in letzter Zeit häufiger mal aus ganz anderen Bereichen, nämlich bei so über die Jahrzehnte oder Jahrhunderte ungelösten mathematischen Problemen, die plötzlich irgendwie mit so einem SMT-Solver auf riesen Rechnerfarben irgendwie gelöst wurden. Aber was ich zeigen will, ist, dass wir diese riesen Rechnerfarben für unsere Standardprobleme in der Programmierung nicht brauchen und dass wir mit einfachen, automatisierten Ansätzen schon wahnsinnig weit kommen. Dann gibt es irgendwie so ein bisschen eine Unterscheidung dazwischen, ob es jetzt irgendwie ein eigenes abgeschlossenes System ist, indem ich Sachen beweise, die häufig auch ihre eigene Programmiersprache mitbringen, die aber häufig dann nicht dazu geeignet ist, irgendwie etwas auszuführen oder maximal irgendwie sich noch irgendwie extrahieren lässt oder ob ich eine konkrete Programmiersprache habe, in der ich meine gesamte Implementierung auch mache und die ich einfach erweitere vielleicht oder wo ich von vornherein irgendwie ein Beweissystem mitdenke. Bei den interaktiven Theorienbeweisern gibt es noch irgendwie so ein Taktiksystem. Häufig Taktiken sind Schritte in einer Beweisführung oder auch Algorithmen, die mich sozusagen in meiner Beweisführung weiterbringen und häufig braucht man die nicht, aber sie sind eine Vereinfachung, dass ich schreibe Werte in meinem Programm und baue dadurch Stück für Stück konstruktiv einen Beweis auf und wenn ich solche Taktiken verwende, dann ist das alles eher ein bisschen versteckt und ich habe eine domain-specific Language meistens, die mit solchen Taktiken, wo ich den gleichen Beweis führen kann, wie wenn ich Stück für Stück mein Beweis, mein Beweisobjekt ist es in den meisten Fällen aufbauen und das irgendwie manuell machen. Das heißt an vielen Stellen kann dieses Taktiksystem dafür sorgen, dass ich einen Beweis schreiben kann, der so einfach wie so ein Beweis auf ein Zettel ist oder noch einfacher, weil ich einzelne Schritte mir wegautomatisieren kann und in Funktionen packen kann und da sehen wir auch schon wieder die Parallelität zur der Programmierung, dass ich auch bei meinen Beweisen kann ich Refactoren und Sachen abstrahieren und im Endeffekt ist es auch nur ein Software-System. Und häufig bauen solche Systeme auf Dependent-Types oder Refinement-Types auf. Dependent-Types ist eine richtig feste Erweiterung von einem Typ-System und da haben wir ja schon ein paar Teile vorhin in dieser Gegenüberstellung gesehen und das bedeutet aber bei den Dependent-Types auch, dass meine Werte sich ebenfalls irgendwie verändern. Zum Beispiel gibt es diese Product-Types, das sozusagen wie ein normaler Wert, den ich sonst auch in meinem Programm hätte, also irgendwie ein Integer zum Beispiel und der ist in einem Product, also in einem Tupel zusammengefasst mit einem Beweis über diesen Wert. Also ich könnte sagen, das hier ist ein Int und das ist aber ein Int, der das beweisbar an dieser Stelle eine Primzahl ist oder was so großes wie die Länge von der Liste oder sowas. Und dann muss ich aber eben in meinem Programm, muss ich zum Beispiel diese Tupel, muss ich dann auch auseinander bauen und muss immer diesen Beweis mitdenken und so weiter und so fort und bei den Refinement-Types ist eher, würde ich sagen, ein Zusatz zu einem Typ-System. Das kann man auch in andere Typ-Systemen mit reinbringen von Sprachen, die bereits existieren und was darauf aufbaut. Das sehen wir dann noch bei Liquid-Teskel später. Gibt es aber mittlerweile irgendwie als experimentelles System auch für Python, habe ich gelesen irgendwo, habe ich aber noch nicht ausprobiert. Gucken wir uns ein paar von diesen Systemen an. Kokos vielleicht ein paar Leuten bekannt, dass diese Kategorie, Theorienbeweiser, die noch seit den 80ern bestehen, aber es ist trotzdem einer der bekanntesten und hat sich natürlich seit den 80ern auch ein bisschen weiterentwickelt und da kommen Stück für Stück auch immer wieder Primitive mit rein, die es einem sehr viel erleichtern. Kokos ganz klar auf der interaktiven Seite, das heißt, dass wenig Automatisierung, ich muss per Hand irgendwie eine Art Beweis kreieren. Lean, es kommt auch vom Microsoft Research, da gibt es jetzt irgendwie dem Nasty Version 4, die es ebenfalls nutzbar macht als eine Programmiersprache und zwar sogar als eine Low Level Sprache. Da kann man so ein bisschen wie ein Rust zum Beispiel, kann man irgendwie an manche Funktionen dranschreiben, dass sie zum Beispiel Partial sind oder dass sie unsafe sind und somit so ein paar Dinge wieder erlauben, die die Sprache eigentlich nicht erlaubt und damit kann ich Systeme irgendwie kreieren, die auf der einen Seite ein bestehendes Programm sind und auf der anderen Seite aber direkt diese Theorem Beweise Sachen mit drin haben und dieses Interaktive ein bisschen mehr automatisiert als in Kok, glaube ich, habe ich noch nicht ausprobiert, auch weil es Lean 4, Lean 4 noch in einem Alpha Stadium ist, aber das ist auf jeden Fall, glaube ich, ein spannendes System, was noch wichtiger werden wird in der Zukunft. Wir bewegen uns Stück für Stück weiter in die automatisierte Richtung. Es gibt F-Star und F-Star ist der Grundbaustein von diesem Project Everest, was wir am Anfang gehört haben. Dieser Verifizierte hatte TP-2-Stack und in F-Star haben sie auch, da gibt es einen Subset, das nennt sich Low Star und das kann man dann zu C-Code generieren. Das ist sozusagen, dass die Basis von diesen verifizierten Kryptoprimitiven, die jetzt im Firefox und im Linux Kern bereits drin sind. Und F-Star ist tatsächlich schon, da werden wir dann noch ein Beispiel sehen, ein ziemlich guter Kompromiss zwischen interaktiv und automatisiert. Ich kann viele von meinen Beweisen, kann ich führen, einfach dadurch, dass ich sage, hier probieren wir das mal zu lösen und wenn es nicht geht, dann übernehme ich halt per Hand mit Taktiken. Ziemlich ähnlich dazu, aber ein anderer Ansatz ist Liquidesco. Liquidesco ist der erste von unseren Sprachen, der auf so einem Refinement System basiert und damit muss ich noch nicht mal meine bestehenden Programme unbedingt anpassen, sondern dass es etwas, was insofern es automatisiert lösbar ist, kann ich das einfach zusätzlich zu meiner Funktionsdefinition dazu schreiben und in den allermeisten Fällen brauche ich gar nichts mehr tun und das ist ziemlich cool. Das ist, ich würde sagen, F-Star und Liquidesco sind irgendwie jetzt in den Codebeispielen, die wir dann noch sehen, wenn wir sehen, dass sie relativ ähnlich sind, auch wenn F-Star noch meiner Meinung nach ein bisschen integrierter ist und ein mächtigeres System ist, was eben auf dependent types mit aufbaut zusätzlich, aber beides ziemlich coole Systeme. Gehen wir noch weiter in die automatisierte Richtung und leider ist es dann so, wenn wir zu sehr in die automatisierte Richtung gehen, ohne dass wir die Möglichkeit haben, irgendwie interaktiv einzuschreiten, dann funktionieren ganz schnelle Sachen nicht mehr, aber das habe ich trotzdem hier mit reingenommen, weil es konkret für C und teilweise für C plus plus Code funktioniert aber nicht so richtig, noch nicht. Gibt es konkret auch Beweistesthemen. Da kann man bestimmte Sachen ganz gut drin beweisen, irgendwie zum Beispiel, dass irgendwie ein Pointer, den ich bekomme, dass der nicht nall ist, was schon ein riesen, riesen Ding ist, würde ich sagen, für ein C-Programm, aber trotzdem kommt man mit diesem rein interaktiven Ansatz nicht besonders weit. Und was vielleicht auch spannend ist, Y3 ist noch mal ein anderer Ansatz, es hat den Ansatz von so einer intermediate representation, also das hat eine Sprache, die sich YML nennt, die ML ähnlich ist, also ähnlich wie OCaml zum Beispiel. Und das ist aber so gedacht, dass ich diese Sprache, wenn ich will, kann ich sie selbst schreiben, aber in den allermeisten Fällen generiere ich aus anderen Sprachen, zum Beispiel aus dem Frama C, was wir vorher gesehen haben, generiere ich diese Sprache und dann kann ich aus dieser Zwischensprache sagen, okay, ich möchte jetzt den Z3-Solver darauf anwenden oder ich möchte den anderen Solver darauf anwenden und ich glaube, dass man auch interaktive Programme dann da anschließen kann. Auch auf jeden Fall ein spannender Ansatz ist aber auch so eine Verstückelungssache, während irgendwie, würde ich sagen, neuere Ansätze es erlauben, all diese Dinge einfach in einer Sprache zu tun. Gucken wir mal, was so übliche Spezifikationen sind, also Sachen, die ich vielleicht gerne beweisen will, die mein Programm macht, das sind, würde ich sagen, auf jeden Fall irgendwie so was wie Protokolle, wo halt irgendwie klar sind, okay, ich habe hier folgende Datenstrukturen, ich habe einen geregelten Ablauf, dieser Schritt darf nur nach diesem Schritt geschehen und auch nur, wenn folgende Bedingungen geschehen sind, dann mache ich das und ansonsten mache ich das. Das ist überall, wo ich eine Design-Dokumentation in irgendeiner Form habe, also wenn ich eine RFC habe, wenn ich irgendwo abstrakt die Eigenschaften oder die Invarianten von meinem System beschrieben habe, vielleicht irgendwie Sicherheitseigenschaften, irgendein rechte Management-System, irgendein Ding, User in Gruppe XY darf nur auf Daten in Gruppe Z zugreifen. All solche Sachen kann ich irgendwie in eine logische Spezifikation überbringen und darüber ein Beweis führen. Dann gibt es natürlich so, so, so, ich sage, natürlich gegebenen Regeln, mathematische Regeln, irgendwie aus der Kryptografie gibt es das ganz viel, dass da einfach feststehende Invariante gibt, physikalische Regeln, wie auch immer, Dinge, die einfach feststehen, die mein Programm abbildet und was ich aber dann gerne auch bewiesen haben möchte. Und es gibt auch so alltägliche Invarianten im Kotor werden wir eben jetzt noch irgendwie ein Beispiel sehen. Das eine ist irgendwie ein Index-Out-of-Bounds-Zugriff. Ich habe irgendeine Struktur, in dem Fall war es irgendwie ein Array, in den Beispielen, die wir jetzt sehen, ist eher eine Liste. Und wenn ich jetzt mit einem Index zugreife, der größer ist als die Länge der Liste, dann greife ich irgendwo hin, wo ich nicht hingreifen darf. In C-artigen Systemen ist es dann irgendwie für das vielleicht noch nicht mal direkt dazu, dass es irgendwie abstürzlos wird, sondern das führt vielleicht dazu, dass ich da irgendwie Sachen einlesen kann, die ich eigentlich lesen möchte, in diesem Fall zum Beispiel. In anderen Sprachen kann es aber auch einfach sein, dass mein System abstürzt und auch das möchte ich nicht, natürlich. Gehen wir mal zu einem Beispiel. Ich habe eine Funktion und die holt sich das IT-Element einer Liste und gibt es zurück. Das gibt es in Haskell schon, das ist dieser zweimal Ausrufezeichen-Operatur. Und der macht auch überhaupt keine Überprüfungen, wenn ich da irgendwo drüber greife, dann gibt es einfach eine Exception. Und weil wir jetzt jetzt aber gerne selber machen wollen, ist hier erstmal die Typ-Definition. Also ich habe diese GetEnd-Funktion, das doppelte Doppelpunkt, zeige ich, dass dahinter mein Funktionstyp kommt. Das heißt, ich habe zwei Parameter, ein Index und eine Liste von Elementen vom Typ T, zum Beispiel ebenfalls Ints oder Strings, je nachdem, was in dieser Liste drin ist. Und dann möchte ich natürlich, wenn ich an einen validen Index gegriffen habe, möchte ich ein Objekt von diesem Typ T zurück. Und weil wir jetzt irgendwie uns in hauptsächlich funktionalen Sprachen bewegen, wählen wir irgendwie eine Rekursive herangehensweise, wir gucken uns an, irgendwie, wenn wir das erste Element in dieser Liste suchen, dann geben wir halt das Kopf-Element zurück und ansonsten decrementieren wir i und rufen GetEnd einfach mit der Restliste auf. Normale Rekursive herangehensweise. Und das Problem habe ich eben schon geschildert, i kann jetzt groß fast die Länge der Liste sein. Gucken wir uns mal an, wie dieses Beispiel einfach in normalem Haskell aussehen könnte. Da haben wir den ersten Fall, der nie auftreten darf, dass ich mit irgendeinem Index in eine leere Liste reingreife. Und dann haben wir den zweiten Fall, dass jetzt mit dem Index, der nicht null ist, nee, dass ich mit einem Index in eine nicht leere Liste reingreife. Und wenn mein Index null ist, also wenn ich das aktuelle Element haben möchte, dann gebe ich das zurück und ansonsten decrementiere ich den Index und mache mit der Liste weiter ohne das Kopf-Element. Genau, aber wenn ich jetzt hier darauf zugreife mit dem Index, der größer ist als die Länge der Liste, dann crash das. Und das unschön und das wollen wir verändern in diesem relativ einfachen Beispiel. Genau, das ist unser Problemfall. Und wir sehen hier schon, wir haben zwei Fallunterscheidungen, das wird dann gleich noch interessanter. Zum einen machen wir eine Fallunterscheidung über den einen Parameter, nämlich über die Liste und gucken uns an, ob die Liste leer ist oder ob da ein Element und eine Restliste drin ist. Das sind die beiden auf der linken Seite und auf der rechten Seite machen wir dann noch eine Fallunterscheidung über unseren Index, ob der null ist oder ob der größer als null ist. So sieht das Ganze in Kock aus und keine Angst, jetzt erstmal riesiger Code batzen, wir gehen das Stück für Stück durch. Wir haben hier die Funktionsdefinition GetEnf und als Teil von GetEnf haben wir einen Beweisobjekt und das schleppen wir mit dieses Beweisobjekt. Das ist sozusagen einfach ein weiterer Parameter von meiner Funktion und dieser Parameter ist irgendeine Struktur, die einen Beweis beinhaltet, dass mein Index kleiner als die Länge der Liste ist an diesem Punkt. Und das ist genau das, was wir wollen, das wollen wir zeigen. Und wir haben das hier an dieser Stelle, ist es so geregelt, dass wir drei normale Parameter haben, nämlich T, dass einfach der Typ von den Elementen der Liste ist. Das habe ich hier in so Curly Braces gemacht, damit der automatisch erkannt wird, dass sozusagen einfach die Polymorphie, dass man das nicht nur mit einer Liste von ins machen kann, sondern auch mit einer Liste von Strings und so weiter. Dann haben wir die Liste selbst, dann haben wir den Index und dann machen wir, gibt es hier sozusagen unseren Typ-Operator, das Doppelpunkt, der mir anzeigt, dass ich eine Funktion zurückbekomme von meiner Funktion. Also wenn ich jetzt meine Funktion mit Typ, Liste und I aufrufe, dann bin ich dann noch nicht fertig, sondern ich kriege eine anonyme Funktion zurück. Und da muss ich mein Beweis reingeben und kriege dann dafür mein Objekt T, mein Element der Liste, an dieser Stelle I zurück. Und wieso man das jetzt so macht, wird sich dann unten nochmal zeigen, dadurch, dass wir eben dieses Proof-Objekt, dass wir das ein bisschen detaillierter schreiben können. Wir haben hier auch wieder unsere Fallunterscheidungen. Wir unterscheiden bei der Liste auf den Nihl-Fall, also ich habe eine Lehre-Liste, was der Fall ist, den wir nicht wollen und unser Fall, der niemals auftreten darf, dass ich mit einem Index in eine Lehre-Liste rein renne. Und dann haben wir den zweiten Fall von dieser Unterscheidung, dass die Liste einer Nicht-Lähre-Liste ist. Und dann machen wir noch eine Fallunterscheidung über das I, nämlich ob das 0 ist oder größer als 0. Und das machen wir hier mit diesen, das nennt sich Piano-Numbers, hat vielleicht jemand schon mal gesehen, dass wir die natürlichen Zahlen nicht als 0, 1, 2, 3, 4 definieren, sondern als entweder 0, dass hier mit diesem O symbolisiert oder mit dem Nachfolger, also dem Successor, deshalb S, von einer natürlichen Nummer. Das heißt, meine natürliche Zahl 2 kann ich repräsentieren als S von S von 0, also der Successor von Successor von 0. Genau, und diese Unterscheidung mache ich hier genauso wie ich die vorhin in Heske mit dem I gleich 0 gemacht habe, mache ich die einfach mit dem Pattern-Match auf dem I, also entweder ist mein I 0 oder es ist ein Successor von irgendeiner anderen Zahl. Gucken wir uns mal den Fall an, der niemals auftreten darf. Das muss sich Coq beibringen, dass dieser Fall niemals aufkommen darf und deshalb habe ich sozusagen einen Randbeweis geführt. Und dieser Randbeweis, wir sehen hier, wenn wir in das Match gucken und das Niel, dann ist unser Proof-Objekt, weil ich sozusagen die Liste, die ich oben noch habe, als Length von LS, dies an dieser Stelle schon Niel. Das heißt, ich habe meinen, deshalb mache ich das auch sozusagen mit dem, dass ich eine anonyme Funktion dazu zurückgebe, weil ich dann sagen kann, okay, mein Proof-Objekt ist hier schon ein konkreteres. Ich weiß jetzt, dass an dieser Stelle die Liste Niel ist und deshalb kann ich in meinem Beweis I kleiner als Length von LS, kann ich jetzt für LS Niel einsetzen und habe deshalb einen konkreteren Beweis, nämlich I kleiner als Length von Niel. Und da sieht man schon, also ich habe eine natürliche Zahl, die niemals kleiner Null sein kann und mein Beweis sagt mir jetzt, dass ich, dass mein Index, der niemals kleiner Null ist, kleiner als der Length, die Length von der leeren Liste ist, der Länge in der leeren Liste ist Null und dementsprechend das I kleiner Null ist Schwachsinn. Das heißt, es ist ein Fall, der nie auftreten kann, wenn ich meine Funktion aufgerufen habe mit so einem Beweis-Objekt, was nie existiert, dann existiert auch dieser Fall nicht. Und dass das nicht geht, das zeige ich hier in dem oberen Fall. Dieses Lämmer, was ich da definiert habe, not less than zero length, das ist das NL-TZL, habe ich kurz gefasst, kann man auch insgesamt wahrscheinlich ein bisschen schöner schreiben, aber was ich da zeige ist, dass wenn ich ein Typ T habe und ein Index I vom Typ Nutt, dann ist mein I, wenn mein I kleiner als Length von Niel ist, dann ist meine Aussage falsch. Das schreibt man so rum in Logik, dass wenn ich eine falsche Aussage habe, dann mache ich die Aussage und mache eine Implikation, schreibe Fals dahinter. Weil Fals, das haben wir vorhin schon gelernt, ist ein Typ, der keine Werte hat. Das heißt, wenn mein Drückerbewert Fals ist, dann kann das nie passieren, weil ich nie etwas zurückgeben kann, weil mein Fals Typ leer ist. Und was man hier sehen kann, ist, dass ich meinen Beweis, den ich unten mache, den irgendwie Schritt für Schritt, das sehen wir dann noch sehen, irgendwie und baue den das Programm mit ein, aber hier kann ich, wenn ich möchte, meine Funktion ebenfalls mit diesen Taktiken beschreiben. Und da würde ich kurz mal wechseln hier in die COC-IDI, da habe ich dieses Beispiel noch mal drin. Und da sieht man es ein bisschen besser, wie das dann aussieht. Ich kann hier Schritt für Schritt, kann ich dieses Programm durchgehen und mit Grünen zeichter mir immer an, dass er bis zu diesem Schritt ist das System mitgekommen und kann meine Aussagen verstehen, wie ich da getroffen habe. Und wenn ich jetzt diese Spezifikation hier habe, dann erscheint die auf der rechten Seite als ein Ziel. Und dieses Ziel kann ich dann beweisen mit diesen Taktiken. Da mache ich jetzt erstmal diesen Intros-Schritt, der mir einfach alle, ich zeige noch mal, was sich ändert, der nimmt einfach alle meine Variablen, die ich hier mit den Quantifizierern definiert habe, also das A und das I und meine Hypothese, also der erste Teil dieser Implikation und definiert die als Hypothesen. Alles, was über dem Strich sind, sind Hypothesen. Was unten ist, das möchte ich beweisen. Und da ist schon, ich möchte false beweisen, das geht schon mal nicht. Man kann nicht false beweisen, weil man keinen Element für false finden kann, weil es das nicht gibt. Deshalb muss man einen anderen Ansatz wählen, nämlich, dass man zeigt, dass eine der Hypothesen falsch ist, von denen ich ausgehe. Und unsere Hypothese hier, je kleiner als Length von Niel, die ist falsch. Und das wollen wir zeigen, deshalb machen wir erstmal einen Schritt, der diese Hypothese simplifiziert. Und das zeigt schon mal I kleiner Null und da gibt es dann eine Taktik, die es auflösen kann, die mir sagt, okay, das kann nicht stimmen, I kleiner Null und das ist diese Inversion-Taktik. Und wenn ich die wähle, dann habe ich dieses Lämmer da oben bewiesen, weil ich gezeigt habe, diese eine Hypothese, von der ich ausgegangen ist, die ist übrigens falsch. Und es ist total egal, was das Gold ist. Genau. Und dann kann ich im Infact zeigen, dass auch erscheint der Ziel grün, dass meine Funktionen, definitiv eine Funktion ist, die mein Beweis da beweist und kann dann auch zeigen, dass die, wenn ich hier auf ein valides Programm zugreife, also wenn ich auf die Stelle 2 zugreife in dieser Liste, dann passt das noch. Und wenn ich auf die Stelle 3 zugreife, dann ist es nicht ganz so schön, was mir nicht anzeigt, das funktioniert nicht, sondern es zeigt mir einfach nur an, neuer, wenn du diesen Beweis hast, dann kannst du das schon machen. Aber da man den nicht konstruieren kann in unserem Programm, das heißt, ich kann keine anderen Funktionen definieren, die mir so einen Beweis mitgibt und diese Funktion dann aufruft damit, ist das nicht dich. Von daher, das funktioniert nicht sozusagen. Jetzt muss man hier wissen, aber das wird man ja auch, das sieht man, wenn man damit rum spielt. Genau, das waren die Taktiken und jetzt noch zu guter Letzt, ich habe hier dieses Proof-Objekt und diesen ersten Fall haben wir jetzt schon irgendwie abgehandelt und diesen zweiten Fall, da habe ich schon sozusagen zwei Sachen ausgetauscht, da habe ich zum einen ausgetauscht, dass jetzt irgendwie die Länge der Liste ohne das Kopf-Element ist sozusagen das gleiche wie, ne, wenn man das plus eins rechnet, also den Successor nimmt, dann kommt man bei dem gleichen raus wie oben das Length von LS, also das ist die Länge von der Gesamtliste mit dem Kopf-Element, das ist irgendwie klar, aber das muss man irgendwie in diesem, beim Pettermatching muss man das Kok beibringen und das checkt er dann und diesen Schritt kann er sozusagen gedanklich mitgehen, das System und dementsprechend können wir dann mit dieser veränderten Aussage hier weitermachen. Genau und das zweite, was wir gemacht haben, ist, dass wir einfach unseren Index i ausgetauscht haben durch das, was wir hier gepettert matched haben, nämlich, dass das der Successor ist von einem i-Strich, was sozusagen der Vorgänge einfach ist. Und was ich dann machen kann, ist, dass ich auf dieses Proof-Objekt, das sieht man hier unten, kann ich, das ist einfach in Kok schon drin, irgendwie eine Funktion, die mir die einen Beweis nimmt und einen anderen Beweis dafür herstellt oder mir einen anderen Beweis dafür rückgängig macht, zurückgibt und das ist in diesem Fall dieses LTSN und was das macht, ist, dass es einfach die beiden S wegnimmt. Also das bedeutet sozusagen, wenn ich irgendwie den Successor habe von natürlichen Zahlen, der ist kleiner als das Successor von einer anderen natürlichen Zahl, dann kann ich dieses beide Successor, kann ich wegnehmen und gut ist. Und was wir damit dann haben, ist den Beweis für den nächsten Rekursiven Schritt. Das heißt, wir können jetzt get ends, können wir aufrufen mit unseren, mit unserer Restliste und dem um eins verminderten i und haben direkt sozusagen ein Beweisobjekt für den nächsten Schritt. Jetzt kann man sagen, puh, also das war unser Ausgang und das soll es jetzt sein, keine Ahnung, das ist cool, um zu wissen, was da eigentlich abgeht und ohne Frage. Aber es geht auch so, dass das ganze Ding in Liquid Haskell und das kennen wir irgendwie schon. Wir könnten jetzt sogar, dieser Fall, der niemals vorkommt, das habe ich ja da auf Andy Feind gesetzt, den könnten wir auch weglassen und was ich sogar noch weglassen könnte, ist den Haskell-Typ an sich, also die zweite Zeile, weil die inferiert wird. Aber was ich jetzt hinzugefügt habe, ist eine Annotation, dass dieses Liquid Haskell und diese Annotation besagt, dass ich schon mal als ersten Parameter übrigens nicht irgendein Int, was auch negativ sein kann, reinnehmen, sondern einen Nut, nämlich das gibt es in Haskell erstmal so nicht, einen Typ, der bei 0 anfängt. Und als zweites kriege ich eine Liste rein und diese Liste ist so groß, dass die Länge der Liste größer ist als mein I. Und das im Infact, würde ich sagen, eine ein bisschen konkretere Typdefinition und mein Liquid Haskell-System haut das in einen Z3 SMT Solver rein und kann mir das auflösen und sagt mir hinterher, okay, passt, du hast irgendwo in deinem Programm hast du diese Funktion aufgerufen mit einem falschen I. Das gleiche nochmal in F-Star, sieht ziemlich ähnlich aus und F-Star ist einfach nur von dem, hier habe ich übrigens den Fall da nicht auftauchen kann, direkt weglassen, kann ich hier auch machen, ist einfach ein bisschen mehr so ML schreibweise, Kammel ähnlich, F-Sharp ähnlich vielleicht auch und ist aber im Infact, wenn wir uns den Typ angucken, was hier ein echter Typ ist, was das coole ist, ist es mehr oder weniger das gleiche. Ich kann sagen, ich kriege ein I rein und das I ist auf jeden Fall größer, gleich null und ich kriege eine Liste rein und die ist so groß, das größer als mein I. Und was ich hier noch sagen kann, dieses Tod, dieses Tod, was ich da vor meinen Typen geschrieben habe, dass ich zurück kriege, heißt, dass das ganze total ist und total bedeutet, dass das terminiert. Und das ist auch ziemlich cool, das kann mein System mitchecken, natürlich was ein ungelöses Problem ist und auch nicht gelösbar ist, die Terminierung, kann es mir das nicht für alles zeigen, aber für die allermeisten Sachen, wo ich es aufzeigen kann. Da ich nicht mehr so viel Zeit habe, an der Usability müssen wir noch ein bisschen schrauben, irgendwie zum Beispiel bei F-Star ist es so, dass man immer exakt ums zu bauen, um die Standard Library zu bauen, exakt eine Version von Z3 haben muss bis auf die letzte Versionsstelle und auch insgesamt die Einordnung in irgendwelche Coding-Systemen nicht so gut. Es gibt viele Beispiele, wo ich relativ easy mit Automatisierung weiterkomme und mein Fazit ist Beweise in Programmiersprachen sind toll und mittlerweile sind sie sogar einfach, weil ich es mir automatisieren kann und selbst an den Stellen, wo ich nicht mehr weiterkomme, kann ich mit Hilfe von Automatisierung weitermachen, was viel einfacher ist. Ich kann meine Interface ganz klar definieren, was da reinkommt und wenn ich einfach Sachen programmiere und das in den 95% der Fall würde ich sagen, dass ich keine riesen logischen Verknüpfungen beweisen möchte, sondern sowas, kaum mehr aufwand. Ich weiß nicht, ob wir Zeit für Fragen noch haben. Eigentlich haben wir keine Zeit mehr. Interessanterweise hast du jetzt auch ein paar Leute, die dir zuhören hier vorgezogen. Ich denke, ihr könnt euch sicher nach unten nochmal Fragen aus dem Chat hatte. Ich habe keine Fragen gefunden. Gut. Ich danke dir erst mal, die Leute, die da sind. Applaus für Marius. Danke.