Institutionen för datavetenskap Department of Computer and Information Science Examensarbete Implementation och utökning av en typhärledningsalgoritm för Common Lisp av Simon Ståhlberg LIU-IDA/LITH-EX-G--10/018--SE 2010-06-04 Linköpings universitet SE-581 83 Linköping, Sweden Linköpings universitet 581 83 Linköping
Linköpings universitet Institutionen för datavetenskap Examensarbete Implementation och utökning av en typhärledningsalgoritm för Common Lisp av Simon Ståhlberg LIU-IDA/LITH-EX-G--10/018--SE 2010-06-04 Handledare: Anders Haraldsson Examinator: Anders Haraldsson
Sammanfattning Programmeringsspråk har oftast ett typsystem och kan välja att använda olika statiska verktyg att analysera koden. Statiska språk utför typkontroller innan körtillfället och kan då ge garantier att typfel inte kan förekomma, dynamiska språk gör däremot dessa kontroller under körtillfället och om ett dåligt beteende upptäcks avbryts programmet. Eftersom kontrollerna görs under körtillfället skrivs inte typsignaturer ut i dynamiska språk. Ett dynamiskt språk kan vara mer exibelt än ett statiskt språk eftersom inga garantier ges. Eftersom inga typsignaturer skrivs ut går det att skriva program på ett mer koncist sätt. I statiska språk tvingas oftast användaren att skriva ut typerna explicit för variabler och funktioner. Typhärledning är processen att härleda typer för uttryck. En typhärledningalgoritm kommer att undersökas, hur kraftfull algoritmen är samt hur algoritmen kan utökas för att klara av era konstruktioner och ett kraftfullare språk. Typhärledningsalgoritmen kommer att implementeras för Common Lisp. Common Lisp ger inga typgarantier och inga typsignaturer skrivs ut. Alla typer kommer att härledas vilket innebär att vi gör om en delmängd av Common Lisp till ett statiskt språk. Algoritmen kommer att utökas för att klara av polymorsm, closures, listor och par. Rapporten undersöker vilka ändringar som behöver göras för att stödja konstruktionerna, hur vi kan göra ändringarna hur de kan implementeras. Fördelar med att ha typhärledning är att användaren slipper skriva ut typsignaturer och kan få ökad produktivitet. Den mest generella typsignaturen kommer alltid att härledas och funktioner som är polymorfa kommer att upptäckas automatiskt. Typhärledningsalgoritmen kommer att även typkontrollera programmet, och kan ge garantier att inga problem kommer att uppstå. Verktyget kommer även att skapa ett nytt program med typsignaturer utskrivna som kompilatorn kan använda för att generera eektivare kod.
Förord Typhärledning introducerades för mig under kursen data- och programstrukturer på Linköping tekniska högskola av Anders Haraldsson. Det kändes magiskt och det var en ögonöppnare att få se att sådan information kan framställas ur själva koden. Jag vill tacka min handledare, Anders Haraldsson, för att ha inspirerat och utformat examensarbetet. Samt för den hjälp som Haraldsson har gett mig under arbetet.
Innehåll 1 Introduktion 1 1.1 Bakgrund............................. 1 1.2 Syfte och frågeställning...................... 3 1.3 Avgränsningar........................... 3 1.4 Struktur.............................. 4 1.5 Förutsättningar och notation.................. 4 2 Teori 5 2.1 Typer och typsystem....................... 5 2.2 Typhärledning........................... 7 3 Implementation 13 3.1 Typer............................... 14 3.2 Omgivningar........................... 15 3.3 Ekvationer och uniering..................... 17 3.4 Substitution............................ 18 3.5 Generera ekvationer........................ 20 3.6 Lambda och högre ordningens funktioner............ 24 3.7 Polymorsm............................ 26 3.8 Listor och par........................... 30 3.9 Topploopen............................ 33 4 Tillämpningar 35 4.1 Typdeklarationer......................... 35 5 Resultat 38 5.1 Analys och slutsatser....................... 38 6 Avslutande diskussion 40 6.1 Diskussion............................. 40 6.2 Framtida arbete.......................... 42
A Källkod 45 B Körexempel 46 B.1 Fakultetsfunktionen........................ 46 B.2 Primtalsfaktorisering....................... 47
Kapitel 1 Introduktion 1.1 Bakgrund För att förstå sig på typer ska vi först ta reda på hur typer uppstår. Ett exempel på ett otypat område är bitsträngar i en dator. Dessa bitsträngar kan ha olika betydelse beroende på hur strängarna används. En bitsträng kan vara en pekare, ett värde eller en instruktion. 'Otypat' betyder egentligen att det bara nns en enda typ [4] och att bitsträngar är den typen. Det är dock oftast orimligt att en bitsträng är tänkt att användas till olika saker. Att använda en pekare som en instruktion är oftast inte meningen att göras. Så fort vi arbetar i ett otypat område börjar vi att ordna och lägga upp bitsträngar i olika typer beroende på syfte [4]. Potentiella typer som redan har nämnts är pekare, värden och instruktioner. Sedan kan värden organiseras till era typer som bokstäver, heltal, yttal, sanningsvärden, funktioner, med mera. Typer uppstår således naturligt. Det är viktigt att inse att datorn i grunden är fortfarande otypat och skiljer inte på bitsträngar. Vi har endast en illusion att det är typat. Ett typfel är när datorn utför en operation med eller på någon bitsträng som är tänkt att användas för något annat. För att garantera att inga typfel kan uppstå, i en dator som annars är otypat, använder vi oss av verktyg som kontrollerar att de operationer som utförs aldrig utförs med eller på fel typer. Denna process kallas för typkontroll. Syftet med ett typsystem är att förbjuda situationer där typfel kan uppstå [4]. Denitionen av typsystem varierar, men följande denition av Benjamin C. Pierce stöter man på ofta och är allmänt accepterad: [A type system is a] tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.[6] 1
Typer kan således ses som en rustning som skyddar den underliggande representationen från oavsiktlig användning [4]. Ett programmeringsspråk där typen av varje uttryck kan bestämmas av en statisk analys av programmet säges vara statiskt typat. Statisk typning är en attraktiv egenskap, men kravet att typerna måste kunna bestämmas vid kompileringstillfället kan begränsa språket för mycket. Kravet kan ändras till ett svagare genom att garantera att alla uttryck kommer att vara typkonsekventa trots att typen inte är känd vid kompileringstillfället. Typkonsekvens menas att om ett typfel uppstår så stannar programmet. Programmet är konsekvent på så sätt att operationerna alltid utförs på rätt typer. Detta implementeras oftast genom att utföra typkontroller under körtillfället. Ett programmeringsspråk där typkontrollen utföres under körtillfället säges vara dynamiskt typat. Det nns fördelar och nackdelar med att göra typkontrollen vid båda tillfällena. Dynamiskt typade språk kan vara exiblare, dock ges det ingen garanti att inga typfel kommer att uppstå. Statiskt typade språk kan således hitta era felaktigheter i programmet utan att ens exekvera det. Istället ges garantin att om ett typfel uppstår kastas ett fel. Statiskt typade språk kan optimera den kompilerade koden; inga typkontroller behöver göras under körtillfället och inga typsignaturer behöver associeras till alla värden (mindre minnesanvändning). Genom att också veta hur mycket plats ett värde kommer att ta kan andra optimeringar göras. Statiskt mot dynamiskt typade språk är ett omdebatterat område. Dynamiskt typade språk har också fördelen med att de kan utelämna typsignaturer i koden då det enda viktiga är att alla värden har rätt typ vid alla tillfällen. Detta låter användaren att skriva program på ett mer koncist sätt. I statiska språk tvingas oftast användaren att skriva ut typerna explicit för variabler och funktioner. Vi kommer då in på typhärledning. Typhärledning är processen att härleda typen av ett uttryck. Ett statiskt språk med typhärledning har då alla fördelar som statiska språk har men samtidigt är det möjligt att skriva program på ett mer koncist sätt genom att utelämna typdeklarationer. För att demonstrera konceptet med typhärledning kan vi titta på fakultetsfunktionen med typsignaturer skriven i Haskell: f a c t o r i a l : : I n t e g e r > I n t e g e r f a c t o r i a l n = i f n > 0 then n f a c t o r i a l ( n 1) e l s e 1 Om vi använder oss av typhärledning kan vi ta bort typsignaturen och skriva fakultetsfunktionen som följande: f a c t o r i a l n = i f n > 0 then n f a c t o r i a l ( n 1) e l s e 1 Typsignaturen kommer att härledas av typhärledningsalgoritmen. 2
1.2 Syfte och frågeställning Det nns era fördelar med att ha en typhärledare till Common Lisp. Först och främst får vi typsäkerhet och garanterar att inga typfel kan ske. Felaktigheter kan även hittas i programmet om vi gör en statiskt typhärledning. Vi kan även få en prestandaökning genom att lägga till typdeklarationer i programmet som kompilatorn kan ta del av. Vi ställer frågan om det är rimligt att göra en typhärledare till ett dynamiskt språk. Rapporten utforskar de algoritmer som nns idag för typhärledning och undersöker en av algoritmer som nns i detalj. En av algoritmerna kommer att implementeras för en delmängd av Common Lisp. Vi utreder även vad som går och inte går att typhärleda samt vilka begränsningar vi måste göra på språket. Algoritmen kommer att utökas för att stödja nya konstruktioner och vi kommer att undersöka vilka konsekvenser det ger. Vid Linköpings tekniska högskola används Common Lisp och Scheme som första programmeringskurs för C, D och Y programmen. Kurserna fokuserar på funktionell programmering och introducerar rekursion, listor, högre ordningens och lambda funktioner samt closures. Verktyget skulle kunna användas av studenter för att typkontrollera sina program eller användas som en laboration i en kurs som följer den klassiska SICP-boken (Structure and Interpretation of Computer Programs) [3]. 1.3 Avgränsningar Vi kommer undersöka och implementera en typhärledningsalgoritm och vi kommer att koncentrera oss på att typhärleda den funktionella delen av Common Lisp. De utökningar som vi gör på typhärledaren kommer inte att matematiskt bevisas att vara korrekta. Vid typfel kommer vi inte att skriva ut var eller vad för typfel som uppstod, utan bara att det nns ett eller era. Indata till typhärledaren antas vara syntaktiskt korrekta program och fullständigt makroutvecklade. Vi antar också ett förenklat Common Lisp. Symbolerna nil och () är sanningsvärdet falskt. I Common Lisp är allt skilt från nil sant, vi antar att endast symbolen t är sanningsvärdet sant. Uttrycken ' nil och '() innebär en tom lista. Symbolen nil får inte heller returneras implicit, exempelvis av en tom progn eftersom vi inte kan veta om det är menat att vara ett sanningsvärde eller en tom lista. Grenarna av ett if -uttryck måste ha samma typ, samt att båda grenarna måste nnas. Uttrycket let måste tilldela ett värde till variabeln. Vi tillåter inte hellre några destruktiva operationer. 3
1.4 Struktur Rapporten tar först upp en komprimerad version av den teori som nns idag för typer och typsystem. Typer och typsystem är ett forskningsområde och rapporten kommer att endast beröra området ytligt. Sedan går rapporten in på typhärledning, vilka algoritmer som nns och en av algoritmerna i detalj, med exempel som visar vad algoritmen klarar av. Därefter implementeras typhärledaren, där teorin översätts till en implementation och utökas för att stödja era konstruktioner. De utökningar som tas upp är författarens egna utökningar och ingår således inte i grundalgoritmen. Verktyget i sig får även stöd för att skapa ett nytt program med typdeklarationer utsatta i programmet. I kapitlen diskuteras även olika lösningar för problemen, och programexempel ges för att illustrera problemen. Avslutningsvis analyseras verktyget, vad vi har gjort och klarar av samt vad som behöver förbättras. De problem och uppgifter som återstår diskuteras samt hur dem kan tänkas att lösas. 1.5 Förutsättningar och notation Rapporten förutsätter att läsaren har kunskaper om Common Lisp och SICPmodellen [3], samt grundläggande matematiska kunskaper. Notationen X Y betyder att X ska ersättas med Y i en substitution. Abstrakta datatyper kommer att följa en namngivning. För en abstrakt datatyp X med fälten Y och Z kommer denna namngivning för konstruktor, selektorer och tester att följas: Konstruktorn har namngivningen: make X Selektorerna har namngivningen: X Y samt X Z Tester har namngivningen: X? 4
Kapitel 2 Teori Vi kommer att titta på typer och typsystem. Hur typer uppstår och vad ett typsystem är, samt vad vi kan uträtta med ett typsystem. Sedan kommer vi att undersöka en typhärledningsalgoritm. För att underlätta undersökningen denerar vi ett språk som vi kallar µscheme. Vi går igenom typhärledningsalgoritmen för µscheme, ser hur algoritmen fungerar och vad den klarar av. 2.1 Typer och typsystem I en dator nns det bara en typ och det är en bitsträng. Datorn skiljer inte på instruktioner, pekare, heltal med mera men typer uppstår naturligt när vi börjar skilja och organisera data. Det är inte rimligt att använda data som vi har tänkt att vara pekare som instruktioner. Eftersom detta inte är rimligt har vi därmed två olika typer. Det är vanligt att se typer som en uppskattning för vilka värden som kan antas eller en abstraktion utav en mängd värden. Denitionen av typsystem varierar, men följande denition av Benjamin C. Pierce stöter man på ofta och är accepterad: [A type system is a] tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.[6] Ett typsystem tilldelar alltså typer till värden och bevisar att vissa beteenden inte kan ske med hänsyn till typerna. Varje uttryck i programmet kommer att ha en typ. Vi vet inte det exakta värdet men vi vet att uttrycket kommer att anta ett av värdena ur mängden. Eftersom vi vet detta kan vi välja att inte tillåta program där den förväntade mängden (typen) inte är en delmängd av den givna för något uttryck. 5
Exempelvis kommer följande program inte att tillåtas: (+ 5 ( lambda ( x ) x ) ) Eftersom addition förväntar sig element ur mängden av alla komplexa tal och funktionsobjekt inte ingår i den mängden kommer uttrycket inte att godkännas. Vi skulle kunna välja att inte tillåta program där division med noll kan ske om noll är ett av värdena i typen, men detta ger ett alltför begränsat språk och eftersom typer endast är en approximation tillåter vi oftast denna risk. Att utforma ett typsystem är en övervägning. Genom att ha mer information kan vi uppskatta programmets beteende mer noggrant och därmed kan vi neka mindre korrekta eller godkänna färre felaktiga program. Dock kommer detta med en kostnad eftersom att få denna statiska information kan kräva att vi måste; göra språket mindre expressivt, göra beräkningar dyrare eller tvinga programmeraren att ge extra information i programmet. Det är viktigt att typsystemet är utformat på ett sådant sätt att vi endast nekar felaktiga och inte giltiga program. Ett dåligt utformat typsystem kan neka många giltiga program, vilket leder till frustration för programmeraren om han/hon måste slåss mot typsystemet för att få igenom sitt program. Ett bra utformat typsystem minskar däremot tiden för felsökning. Typkontroll är processen att veriera och upprätthålla restriktioner för typer. Denna kontroll ingår i typsystemet men programmeringsspråk kan välja att göra denna kontroll vid två olika tillfällen: antingen vid kompileringstillfället (statisk) eller körtillfället (dynamisk). Ett språk som gör denna kontroll vid kompileringstillfället eller vid körtillfället sägs vara statiskt typat respektive dynamiskt typat. Det nns fördelar och nackdelar med båda metoderna. Fördelar med att göra kontrollen vid kompileringstillfället är att man kan fånga upp fel innan programmet ens har exekverats och kunna bevisa att inga typfel kommer att kunna ske under körningen. Om man kan ge denna garanti sägs språket vara typsäkert. 1 Kompilatorn kan även ta del av denna garanti och anta att alla värden har rätt typ vid alla tillfällen. På så sätt kan kompilatorn göra optimeringar som att ta bort typkontroller och veta hur mycket minne som behöver allokeras. Nackdelar med statisk typkontroll är att giltiga program kan nekas eftersom de inte vid det statiska tillfället kan avgöras vara typsäkra eller inte. Med andra ord kan statiska språk behövas vara mindre exibla än dynamiska för att kunna ge denna garanti. Fördelar med att göra kontrollen vid körtillfället är som sagt att språket kan bli exiblare än statiskt typade språk. Det enda kravet är att alla värden har rätt typ vid alla tillfällen. Eftersom det är det enda viktiga behövs inga 1 Många statiska språk har kryphål som gör att programmet kan få typfel. 6
typsignaturer i programmet, detta gör att man kan skriva sina program mer koncist och därmed ökar produktiviteten. Nackdelar med dynamisk typning är att ingen garanti kan ges att inga typfel kommer att ske och därefter måste vi göra typkontroller under körtillfället vilket kostar processorkraft. Vi måste även spara typinformation för varje värde, och så kräver det mer minnesutrymme. Eftersom ingen garanti ges kompenseras detta oftast genom unit testing, på så sätt hoppas vi fånga upp typfelen under testerna istället men ändå ha tillgång till det mer exibla språket. Det är dock svårt att få samma täckning som statisk typkontroll. 2.2 Typhärledning I många statiska språk behöver användaren skriva ut typsignaturer för uttryck, men detta tvingar användaren att skriva ut information i form av typdeklarationer om koden, i koden. Detta är dock inte nödvändigt då informationen redan nns i koden implicit; vi kan härleda typsignaturer från användningen och sammanhanget. Vi skulle alltså kunna skriva ett program utan typdeklarationer likt de dynamiska språken men ändå ha typsäkerhet. Typhärledning går löst ut på att inspektera hur uttryck sitter ihop och operationer används. Utifrån att vi vet vilka typsignaturer de operationer vi använder har, kan vi härleda vilka krav som ställs på den nya funktionen. De vanligare algoritmer som nns idag är Damas-Milner (kallas även algoritm W) [5] och Wands [9] typhärledningsalgoritmer. I rapporten kommer vi att titta närmare på Wands algoritm. Algoritmen är förvånandsvärt enkel. Det nns två steg: första steget är att generera ekvationer och det andra är att lösa dessa. Vi kommer att ändra lite i algoritmen för våra ändamål, så de bevis som nns i Wands artikel [9] blir inte giltiga. Algoritmen är egentligen för lambdakalkyl. För våra ändamål kommer vi istället att använda ett eget språk likt Scheme som vi kallar µscheme. µscheme har tre bastyper: sanningsvärden, heltal och funktioner. Funktioner tar ett argument och returnerar ett värde (alltid samma typ). Symbolerna för sanningsvärdena är true och false. Denition 1. V är mängden av alla variabler (typiskt x, y, z, e.t.c.). Λ är mängden av alla program i µscheme och denieras på följande vis: 1. V Λ 2. Z Λ 3. true, false Λ 7
4. E 1, E 2 Λ (E 1 E 2 ) Λ 5. E 1, E 2 Λ (= E 1 E 2 ) Λ 6. C, E 1, E 2 Λ (if C E 1 E 2 ) Λ 7. E Λ, x V (lambda (x) E) Λ 8. E 1, E 2 Λ, x V (let (x E 1 ) E 2 ) Λ Lägg märke att det ingår program som inte är typkorrekta, exempelvis (lambda (x) (5 x)) ett syntaktiskt korrekt uttryck men 5 är uppenbarligen ingen funktion och är således inte typkorrekt. Inga typsignaturer skrivs ut i detta språk. Vi skulle kunna deniera ett liknande språk där typsignaturerna skrivs ut, uppgiften för typhärledning blir då att översätta från ett språk till ett annat och fylla i de typsignaturer som saknas. Denition 2. T V = {τ 1, τ 2,... } är mängden av alla typvariabler. T E är mängden av alla typuttryck som deneras på följande vis: 1. {Integer, Boolean} T E 2. T V T E 3. t 1, t 2 T E (t 1 t 2 ) T E Mängden T E är alltså alla olika möjliga typsignaturer vi kan ha. Exempelvis Integer Boolean och (τ 1 Integer) Integer ingår i T E. Vi har endast funktioner som tar ett argument, en funktion som tar ett godtyckligt antal argument har typsignaturen t 1 t 2 t n t. Låt oss inte luras att ett argument är en begränsning, vi kan skriva om funktioner som tar ett godtyckligt antal argument till en funktion som tar ett argument genom att låta den nya funktionen returnera en funktion som tar det andra argumentet (och så vidare). Detta kallas för currying 2. Denition 3. A : V T E är en partiell funktion med en ändlig domän. Vi kallar A för typomgivning.vi kommer att skriva A[x : t] för att utöka funktionen A där variabeln x som är associerad med typuttrycket t. Typomgivningen fungerar likt omgivningar i programmeringsspråk (avbildar variabler till ett värde normalt), men avbildar till ett typuttryck istället. Algoritmen för att generera ekvationer består av två komponenter; en loop för att stega igenom programmet och en tabell med regler. Vi kommer att hänvisa denna algoritm som Wands algoritm. 2 Namngivit efter Haskell Curry. 8
För att gå igenom programmet används följande algoritm: Indata: Ett uttryck E 0 Λ Initalisering: Sätt EQ = och G = {(A 0, E 0, t 0 )}, där A 0 är en tom typomgivning och t 0 T E. Loop: Om G =, avsluta och returnera EQ. Annars, välj ett element (A, E, t) från G och ta bort elementet från G. Lägg till nya element i EQ och G enligt tabellen av regler med hänsyn på elementet som valdes. EQ är en mängd med ekvationer. Det är denna mängd som vi kommer att behöva lösa, om det går, för att få ut typuttrycken. I G kommer nya deluttryck av programmet att läggas till med en typomgivning och typuttryck, det går att se denna mängd som en kö av uttryck som vi ska inspektera. Algoritmen är hållbar (sound på engelska, innebär att endast korrekta typsignaturer härleds) och fullständig (complete på engelska, innebär att om uttrycket är typkorrekt så härleds typsignaturen) [9] om tabellen med regler alltid terminerar och bevarar hållbarheten och fullständigheten. Vad vi nu behöver är en tabell med regler som algoritmen kan använda sig av. För µscheme kan tabellen se ut: Fall 1. (A, x, t), där x V. Lägg till ekvationen t = A(x). Fall 1. (A, z, t), där z Z. Lägg till ekvationen t = Integer. Fall 2. (A, (M N), t), där M, N Λ. Låt τ vara en ny typvariabel. Lägg till elementen (A, M, τ t) och (A, N, τ) i G. Fall 3. (A, (lambda (x) E), t), där x V och E Λ. Låt τ 1 och τ 2 vara två nya typvariabler. Lägg till ekvationen t = τ 1 τ 2 och elementet (A[x : τ 1 ], E, τ 2 ). Fall 4. (A, (= E 1 E 2 ), t), där E 1, E 2 Λ. Låt τ 1 och τ 2 vara två nya typvariabler. Lägg till ekvationerna τ 1 = Integer och τ 2 = Integer, samt elementen (A, E 1, τ 1 ) och (A, E 2, τ 2 ). Fall 5. (A, (if C E 1 E 2 ), t), där C, E 1, E 2 Λ. Låt τ 1, τ 2 och τ 3 vara tre nya typvariabler. Lägg till ekvationerna τ 1 = Boolean, τ 2 = t och τ 3 = t, samt elementen (A, C, τ 1 ), (A, E 2, τ 2 ) och (A, E 3, τ 3 ). Fall 6. (A, (let (x E 1 ) E), t), där x V och E Λ. Låt τ vara en ny typvariabel. Lägg till elementen (A, E 1, τ) och (A[x : τ], E, t). 9
Vi matchar elementet vi valde i loopen med reglerna som nns (om era matchar, ta den mest specika). Enligt reglerna lägger vi till ekvationer och element i EQ och G, sedan fortsätter vi loopen. Vi kan exempelvis typhärleda följande uttryck: ( lambda ( x ) ( lambda ( y ) ( i f (= y 0) ( x 5) 1 0 ) ) ) Följande tabell visar hur algoritmen går igenom uttrycket och bestämmer typsignaturen: Typuttryck Typomgivning Uttryck Ekvationer t 0 (lambda (x)... ) t 0 = t 1 t 2 t 2 {x : t 1 } (lambda (y)... ) t 2 = t 3 t 4 t 4 {x : t 1, y : t 3 } (if (= y 0)... ) t 5 = Boolean, t 4 = t 6, t 5 {x : t 1, y : t 3 } (= y 0) t 4 = t 7 t 5 = Boolean, t 8 = Integer, t 9 = Integer t 8 {x : t 1, y : t 3 } y t 3 = t 8 t 9 {x : t 1, y : t 3 } 0 t 9 = Integer t 6 {x : t 1, y : t 3 } (x 5) t 10 = t 11 t 6 t 10 {x : t 1, y : t 3 } x t 10 = t 1 t 11 {x : t 1, y : t 3 } 5 t 11 = Integer t 7 {x : t 1, y : t 3 } 10 t 7 = Integer Tabell 2.1: Exempel på hur typhärledningsalgoritmen beter sig Nu har vi gått igenom hela uttrycket och skapat ekvationerna (alla ekvationer som står till höger). Andra fasen i algoritmen är att lösa ekvationerna och för att lösa göra detta använder vi oss av uniering. Algoritmen kommer ifrån matematisk logik och ger en substitution som gör att vänster- och högerledet ser likadana ut. Om algoritmen misslyckas har ett typfel upptäckts. Genom att använda uniering kommer vi även få den mest generella typen (eftersom vi kommer att få den mest generella substitutionen), kallas även för principal type. Efter unieringen får vi ut följande: Typvariabel t 0 t 1 (x) t 3 (y) Substitution (Integer Integer) (Integer Integer) Integer Integer Integer 10
Typuttrycket t 0 är vad vi börjar med och är därmed typuttrycket för hela uttrycket. Substitutionen som t 0 får är typsignaturen för uttrycket. Uttrycket är alltså en högre ordningens funktion som tar en funktion och returnerar en funktion. Båda funktionerna går från ett heltal till ett heltal. Typuttrycken t 1 och t 3 är för variablerna x respektive y och deras substitution är typsignaturerna för variablerna. Variabeln x ska vara en funktion som går från ett heltal till ett heltal, och y ska vara ett heltal. För att lösa ekvationerna använder vi som sagt uniering. Algoritmen för uniering är [2]: Indata: En mängd ekvationer E. Initialisering: S = där S är en mängd av substitutioner. C = E där C är en stack utav ekvationer. Loop: Om C =, avsluta och returnera S. Annars ta en ekvation X = Y från C: 1. Om X och Y är identiska, gör ingenting. 2. Om X är en typvariabel och X inte förekommer i Y, ersätt då alla förekomster av X med Y i S och C. Lägg sedan till X Y i C. 3. Om Y är en typvariabel och Y inte förekommer i X, ersätt då alla förekomster av Y med X i S och C. Lägg sedan till Y X i C. 4. Om X och Y har formen C(X 1,..., X n ) respektive C(Y 1,..., Y n ) för någon konstruktor C, lägg då till ekvationerna X i = Y i för alla 1 i n på stacken. 5. X och Y unierar inte. Ge ett felmeddelande. Om unieringen inte lyckas innebär detta att ett typfel har uppstått. Uttrycket är således inte typkorrekt. Lägg märke till att detta innebär att en typkontroll utförs implicit i algoritmen. Detta är för att algoritmen endast kan typhärleda giltiga typuttryck och om programmet inte är typkorrekt nns det inget giltigt typuttryck. Omgivningsmodellen för språket tas hand om typomgivningen och kommer att referera till rätt variabel genom den. Om typomgivningen inte är denerad för variabeln (partiell funktion trots allt) betyder det att variabeln inte har deklarerats. Det går bra att lägga in fördenerade funktioner i typomgivningen innan vi typhärleder och kan på så sätt utöka språket på ett väldigt enkelt sätt. 11
Säg att vi vill utöka språket med zero?-funktionen. Vi kan då lägga till i typomgivningen att A(zero?) = Integer Boolean. Detta är allt som algoritmen tar upp [9], men det nns mer att lägga till och vi behöver utöka den. Exempelvis så nns även funktioner som är polymorfa. Polymorfa funktioner har egenskapen att de inte ställer några krav på vilken typ argumenten har, utan fungerar på era. Exempelvis identitetsfunktionen som tar ett värde och returnerar samma värde. Funktionen är polymorsk och har typsignaturen X X, där X innebär att funktionen är polymorf och vilken typ som helst kan substitueras in istället för X. Algoritmen klarar av att typhärleda typsignaturen men ett känt problem med polymorsm är let-polymor. Problemet uppstår när vi använder samma polymorfa funktion era gånger: ( lambda ( ) ( l e t ( i d ( lambda ( x ) x ) ) ( i d 5) ( i d i d ) ) ) Funktionen id är identitetsfunktionen och vid första anropet skapar vi en ekvation som säger att x ska vara ett heltal, vid andra anropet skapar vi en till ekvation som säger att x ska vara en funktion. Detta innebär att vid uniering kommer vi att misslyckas eftersom ett heltal ska unieras med en funktion vilket är omöjligt. Problemet uppstår för att vi använt samma typvariabel för x för båda anropen, men programmet är typkorrekt! För att lösa problemet räcker det i detta fall med att substituera in id:s värde på de platser variabeln förekommer. Unieringen kommer att lyckas och uttrycket är korrekt typhärlett. Lösningen kommer dock att ge problem vid rekursion och är därför ingen bra lösning, men det löste problemet. Det som gjorde att det fungerade var att vid användning av id användes nya typvariabler. Vi vill göra så att när vi i typomgivningen slår upp ett uttryck ska vi också ersätta alla fria typvariabler med nya. Detta fungerar bra så länge det inte nns någon typvariabel som är beroende av något utanför räckviden för funktionen. Vi kommer att återvända till detta problem under implementationen. 12
Kapitel 3 Implementation Common Lisp är ett stort språk som är dynamiskt typat. Det är omöjligt att typhärleda hela Common Lisp eftersom Common Lisp är dynamiskt typat, om det hade gått skulle vi eektivt ha gjort Common Lisp till ett statiskt typat språk. Vi kommer att fokusera på den funktionella delmängden av Common Lisp. De esta språk som använder sig av typhärledning är funktionella språk och stödjer saker som rekursion, polymorsm, homogena listor, tupler, högre ordningens funktioner, algebraiska datatyper, closures (engelska, skulle kunna översättas till tillslutning/stängning) med mera. Vi implementerar en typhärledare som klarar av dessa konstruktioner. Först översätter vi teorin till Common Lisp. Variabler i Common Lisp är symboler skilda från t och nil. Mängden Λ från teoridelen blir mängden av alla syntaxtiskt korrekta program (begränsas dock till de operationer vi stödjer). Vi kommer att implementera algoritmen annorlunda utifrån Wands algoritm, och istället följa SICP-modellen [3] för att slå upp regler och generera ekvationer. Kapitlet kommer att presenteras likt SICP-boken [3]. Vi börjar med att stödja en delmängd av Common Lisp och sedan utökar vi algoritmen för att stödja er och er konstruktioner. De utökningar som görs på algoritmen tas inte upp i Wands artikel [9] och är författarens egna utökningar. Matematiska bevis kommer inte att presenteras för utökningarna och vi vet således inte om dem är korrekta. 13
3.1 Typer Vi skapar en abstrakt datatyp typevariable som håller typvariabeln. Typvariabeln är en symbol som är tänkt att vara unik skapas av gensym. ( defun make t y p e v a r i a b l e ( var ) ( box ' t y p e v a r i a b l e var ) ) ( defun get t y p e v a r i a b l e ( ) ( make t y p e v a r i a b l e ( gensym ) ) ) Funktionen box returnerar helt enkelt en lista taggad med första argumentet och omsluter andra argumentet. Funktionen unbox används för att få ut objektet igen. Abstraktionen används för att skapa egna datatyper som går att göra tester m.m. på. Vi har inga bastyper i samma mening som i teoridelen. Typuttryck och bastyper är starkt sammankopplade. Vi representerar typuttryck som en trippel (name type variables). Attributet name är namnet på typen och ska vara en symbol. Attributet type används för att bygga upp typhierarkin och ska vara ett typuttryck eller en typvariabel. Topptypen (mest generella typen) är innerst och typhierarkin byggs utåt. Undantaget är object som alltid är ytterst för att få polymorfa egenskaper. Attributet variables är en lista av typuttryck som används av typer som listor, funktioner och tupler som behöver mer information. Exempelvis en funktion som behöver information om vilka typuttryck argumenten ska vara och ett typuttryck för vad funktionen avbildar till. ' ( o b j e c t ( i n t e g e r ( t y p e v a r i a b l e x ) ( ) ) ( ) ) ' ( o b j e c t ( l i s t ( t y p e v a r i a b l e x ) ( ( o b j e c t ( t y p v a r i a b l e y ) ( ) ) ) ) ( ) ) Första exemplet representerar typuttrycket för heltal och andra en lista av typen object. Vi skapar även en ny typ object som är unionen av alla typer och agerar som topptypen (vi skulle lika gärna välja att ha en typvariabel som topptyp). Typvariabeln gör det möjligt att uniera ekvationer X = Y där X och Y är olika typuttryck. Om en funktion tar ett argument som är av typen object innebär det att funktionen är polymorsk. Eftersom vi har typvariabler i alla typuttryck klarar vi även av F-Bounded polymorsm. Genom att ge två eller era typuttryck samma typvariabler, exempelvis identitetsfunktionen säger vi att typen som anropas med är samma typ som returneras. Ett annat exempel på F-Bounded polymorsm kan vara funktionen =, som kan kräva att båda argumenten ska vara av samma typ. Med denna representation skulle vi kunna göra en hierarki av typer och tillåta undertypspolymorsm, men det är tyvärr inte möjligt. 14
Det är lockande att representera typhierarkin på följande sätt: ' ( o b j e c t ( complex ( r e a l ( r a t i o n a l ( i n t e g e r ( t y p e v a r i a b l e x ) ( ) ) ( ) ) ( ) ) ( ) ) ( ) ) Och sedan utnyttja unieringsalgoritmen. Exempelvis hade typhärledningsalgoritmen korrekt härlett uttrycket (= x 5), och påstått att x måste vara ett heltal. Dock fungerar det inte med uttrycket ( if x 42 3.14159). Typhärledaren hade påstått att uttrycket ger ett heltal trots att den korrekta och mest generella typen är ett reellt tal. Vi kommer att uniera ett reellt tal med ett heltal, vilket ger ett heltal. Det är uppenbarligen fel. Om vi vänder på typhierarkin löser vi detta problem. ' ( o b j e c t ( i n t e g e r ( r a t i o n a l ( r e a l ( complex ( t y p e v a r i a b l e x ) ( ) ) ( ) ) ( ) ) ( ) ) ( ) ) Anta att funktionerna foo och bar har typsignaturerna Real Boolean respektive X Complex. Uttrycket (foo (bar x)), enligt typhärledaren, skulle lyckas och inte ha några typfel. Detta är uppenbarligen fel då funktionen foo får ett komplext tal. Anledningen till varför det lyckas är för att komplexa och reella tal unierar. Således går det inte att representera typhierarkin på det sättet. Antagligen måste vi utöka unieringsalgoritmen för att kunna förstå typhierarkin och lösa ekvationssystemet på ett annat sätt. 3.2 Omgivningar Common Lisp har två omgivningar, en för variabler och en för funktioner. En abstrakt datatyp skapas för båda omgivningarna som vi kallar för typomgivning och kallar de riktiga typomgivningarna för variabelomgivning och funktionsomgivning respektive. Vi kommer även att ha en omgivning för ekvationer som vi kallar för ekvationsomgivning. Senare i rapporten redovisas det varför vi vill ha en omgivning för ekvationer. ( defun make type environment ( cenv venv f e n v ) ( box ' type environment ( l i s t cenv venv fenv ) ) ) En omgivning består av en ram, en inneslutande omgivning, lista på omgivningar som utökar omgivningen samt det uttryck som skapade omgivningen. Ramarna består av ett par av listor; första listan består av variabel/funktion namn, och den andra av typuttryck. Den inneslutande omgivningen är den omgivning som det utökades ifrån. Vi har valt att spara en referens till de omgivningar som utökar omgivningen eftersom vi inte vill att ramarna ska förstöras efter att de använts. Det är också intressant att spara uttrycket 15
som skapade ramen. Vi kommer att se senare i implementationen varför vi vill spara omgivningarna och uttrycket. ( defun make environment ( frame e n c l o s i n g e x p r e s s i o n &o p t i o n a l ( e x t e n d i n g ( empty extending l i s t ) ) ) ( l i s t frame e n c l o s i n g e x p r e s s i o n e x t e n d i n g ) ) ( defun make frame ( v a r i a b l e s v a l u e s ) ( cons v a r i a b l e s v a l u e s ) ) ( defun extend environment ( v a r s v a l s base env &o p t i o n a l ( e x p r e s s i o n n i l ) ) ( i f (= ( l e n g t h v a r s ) ( l e n g t h v a l s ) ) ( l e t ( ( frame ( make frame v a r s v a l s ) ) ( env ( make environment frame base env e x p r e s s i o n ) ) ) ( cons e x t e n d i n g! env ( extending environments base env ) ) env ) ( e r r o r ' extend environment ) ) ) Vi utökar typomgivningar med extend type environment. Den returnerar en ny typomgivning där de tre omgivningarna som den kapslar in är utökade. ( defun extend type environment ( v v a r s v v a l s env ) ( l e t ( ( cenv ( environment cenv env ) ) ( venv ( environment venv env ) ) ( fenv ( environment fenv env ) ) ( cenv2 ( extend environment ' ( ) ' ( ) cenv ) ) ( venv2 ( extend environment v v a r s v v a l s venv ) ) ( fenv2 ( extend environment ' ( ) ' ( ) fenv ) ) ) ( make type environment cenv venv fenv ) ) ) Den globala typomgivningen är en utökning av den tomma typomgivningen. I den globala funktionsomgivningen kan vi lägga fördenerade funktioner som exempelvis +,, mapcar, list, cdr, cons, equal och så vidare. Det blir väldigt lätt att utöka språket med funktioner genom att lägga dem i den globala funktionsomgivnigen. 16
Tabell 3.1 listar de funktioner som arbetar på typomgivningar, omgivningar och ramar. Namnen är självförklarande. Typomgivningar Omgivningar Ramar get function value extend environment frame variables get variable value the empty environment frame values add variable! empty environment? make frame add function! get global environment add constraint! rst frame get global type environment fetch constraints make global type environment make empty type environment extend type environment enclosing environment environment expression extending environments add binding to frame lookup variable value set variable value! dene variable! make environment Tabell 3.1: Funktioner som arbetar på typomgivningar, omgivningar och ramar. 3.3 Ekvationer och uniering En ekvation består av ett vänster- och ett högerled där leden ska vara ett typuttryck. En abstrakt datatyp constraint för ekvationer skapas. Vi har även en abstrakt datatyp substitution för substitutioner, likt ekvationer har substitutioner ett vänster- och högerled där vänsterledet är en typvariabel och högerledet är ett typuttryck. ( defun make c o n s t r a i n t ( l e f t r i g h t ) ( box ' c o n s t r a i n t ( l i s t l e f t r i g h t ) ) ) ( defun make s u b s t i t u t i o n ( var s u b s t ) ( box ' s u b s t i t u t i o n ( l i s t var s u b s t ) ) ) Unieringsalgoritmen följer nästan exakt algoritmen i teoridelen. Typvariabler motsvarar variabler och typuttryck motsvarar termer. Funktionen list constraints tar två listor och skapar elementvis ekvationer. Uttrycket (rec substitute new old obj) går rekursivt igenom typuttryck och ersätter typvariabeln old med new i obj. Unieringsfel betyder att ett typfel har uppstått och att programmet inte är typsäkert. 17
( defun u n i f i c a t i o n ( s t a c k &o p t i o n a l ( s u b s t s ' ( ) ) ) ( i f ( n u l l s t a c k ) s u b s t s ( l e t ( ( c o n s t r ( f i r s t s t a c k ) ) ( x ( c o n s t r a i n t l e f t c o n s t r ) ) ( y ( c o n s t r a i n t r i g h t c o n s t r ) ) ) ( cond ( ( and ( t y p e v a r i a b l e? x ) ( t y p e v a r i a b l e? y ) ( equal t y p e v a r i a b l e s? x y ) ) ( u n i f i c a t i o n ( r e s t s t a c k ) s u b s t s ) ) ( ( and ( t y p e v a r i a b l e? x ) ( not ( occurs check x y ) ) ) ( u n i f i c a t i o n ( rec s u b s t i t u t e y x ( r e s t s t a c k ) ) ( cons ( make s u b s t i t u t i o n x y ) ( rec s u b s t i t u t e y x s u b s t s ) ) ) ) ( ( and ( t y p e v a r i a b l e? y ) ( not ( occurs check y x ) ) ) ( u n i f i c a t i o n ( rec s u b s t i t u t e x y ( r e s t s t a c k ) ) ( cons ( make s u b s t i t u t i o n y x ) ( rec s u b s t i t u t e x y s u b s t s ) ) ) ) ( ( and ( t y p e e x p r e s s i o n? x ) ( t y p e e x p r e s s i o n? y ) ( equal t y p e e x p r e s s i o n s? x y ) ) ( u n i f i c a t i o n ( append ( cons ( make c o n s t r a i n t ( t y p e e x p r e s s i o n type x ) ( t y p e e x p r e s s i o n type y ) ) ( l i s t c o n s t r a i n t s ( t y p e e x p r e s s i o n v a r i a b l e s x ) ( t y p e e x p r e s s i o n v a r i a b l e s y ) ) ) ( r e s t s t a c k ) ) s u b s t s ) ) ( t ( e r r o r ' u n i f i c a t i o n f a i l u r e ) ) ) ) ) ) 3.4 Substitution Av unieringen fås en substitution. Vi vill substituera på typomgivningen (alla tre omgivningar), för att göra detta denerar vi funktionerna rec substitute! och typeexpression substitute! samt frame substitution!, environment substitution! och ext environment substitution!. Funktionen rec substitute! tar tre argument; det nya typuttrycket, den typvariabel som ska ersättas och ett typuttryck. Den går igen typuttrycket och ersätter alla typvariabler som motsvarar typvariabeln och ersätter med det nya typuttrycket. 18
Funktionen typeexpression substitute! tar två argument; ett typuttryck och en substitution och applicerar substitutionen på typuttrycket. Funktionen frame substitution! tar en substitution, en ram och en funktion. Vi behöver funktionen för att ramarna i funktionsomgivningen är ekvationer och inte typuttryck. Funktionen frame substitution! applicerar substitutionen på varje typuttryck i ramen. För variabel- och funktionsomgivningen ska funktionen vara identitetsfunktionen men för ekvationsomgivningen behöver vi en funktion som tar en lista av ekvationer och returnerar en lista med typuttryck (vänster- och högerleden). Funktionen environment substitution! tar tre argument; en substitution, en omgivning och en funktion. Funktionen anropar frame substitution! på sin ram och inneslutande omgivningars ramar rekursivt. Funktionen ext environment substitution! tar tre argument; en substitution, en omgivning och en funktion. Denna gör tvärtom emot environment substitution! och substituerar rekursivt i de utökande omgivningarna istället. Vi kommer att se senare varför vi vill ha detta. ( defun rec s u b s t i t u t e! ( new o l d t e ) ( l a b e l s ( ( l s t s u b s t! ( l i s t ) ( i f ( not ( endp l i s t ) ) ( rec s b u s t i t u t e! new o l d ( c a r l i s t ) ) ) ) ) ( l e t ( ( type ( t y p e e x p r e s s i o n type t e ) ) ( v a r s ( t y p e e x p r e s s i o n v a r i a b l e s t e ) ) ) ( cond ( ( and ( t y p e v a r i a b l e? type ) ( equal t y p e v a r i a b l e? type o l d ) ) ( s e t f ( t y p e e x p r e s s i o n type t e ) new ) ( l s t s u b s t! v a r s ) ) ( t ( rec s u b s t i t u t e! new o l d type ) ( l s t s u b s t! v a r s ) ) ) ) ) ) ( defun t y p e e x p r e s s i o n s u b s t i t u t e! ( t e s u b s t s ) ( cond ( ( endp s u b s t s ) t e ) ( t ( rec s u b s t i t u t e! ( s u b s t i t u t i o n s u b s t ( c a r s u b s t s ) ) ( s u b s t i t u t i o n i d e n t ( c a r s u b s t s ) ) t e ) ( t y p e e x p r e s s i o n s u b s t i t u t e! t e ( r e s t s u b s t s ) ) ) ) ) ( defun frame s u b s t i t u t i o n! ( s u b s t s frame fun ) ( l a b e l s ( ( aux ( t e l i s t ) ( i f ( not ( endp t e l i s t ) ) ( progn ( t y p e e x p r e s s i o n s u b s t i t u t e! ( c a r t e l i s t ) s u b s t s ) ( aux ( r e s t t e l i s t ) ) ) ) ) ) ( aux ( frame v a l u e s ( f u n c a l l fun frame ) ) ) ' ok ) ) 19
( defun environment s u b s t i t u t i o n! ( s u b s t s env &o p t i o n a l ( fun ( lambda ( x ) x ) ) ) ( l a b e l s ( ( env loop ( env ) ( i f ( not ( empty environment? env ) ) ( progn ( frame s u b s t i t u t i o n! s u b s t s ( f i r s t frame env ) fun ) ( env loop ( e n c l o s i n g environment env ) ) ) ) ) ) ( env loop env ) ' ok ) ) ( defun ext environment s u b s t i t u t i o n! ( s u b s t s xenv &o p t i o n a l ( fun ( lambda ( x ) x ) ) ) ( l a b e l s ( ( aux ( ext envs ) ( cond ( ( empty extending l i s t? ext envs ) ' ok ) ( t ( ext environment s u b s t i t u t i o n! s u b s t s ( c a r ext envs ) ) ( aux ( r e s t ext envs ) ) ) ) ) ) ( frame s u b s t i t u t i o n! s u b s t s ( f i r s t frame xenv ) fun ) ( aux ( extending environments xenv ) ) ) ) Vi denierar även en funktion tenv substitution! som tar en substitution, en typomgivning och substituerar i alla typuttryck i omgivningen och även i alla utökningar: ( defun tenv s u b s t i t u t i o n! ( s u b s t s env ) ( l e t ( ( cenv ( environment cenv env ) ) ( venv ( environment venv env ) ) ( fenv ( environment fenv env ) ) ) ( environment s u b s t i t u t i o n! s u b s t s venv ) ( environment s u b s t i t u t i o n! s u b s t s fenv ) ( environment s u b s t i t u t i o n! s u b s t s cenv ) ( ext environment s u b s t i t u t i o n! s u b s t s venv ) ( ext environment s u b s t i t u t i o n! s u b s t s fenv ) ( ext environment s u b s t i t u t i o n! s u b s t s cenv ( lambda ( x ) ( append ( mapcar #' c o n s t r a i n t l e f t x ) ( mapcar #' c o n s t r a i n t r i g h t y ) ) ) ) ' ok ) ) 3.5 Generera ekvationer Vi kommer inte att implementera Wands algoritm som nns i teoridelen för att gå igenom programmet utan implementerar något som liknar en interpretator. Idén med att separera algoritmen och tabellen i teoridelen är att det är lätt att bevisa att algoritmen kommer att göra rätt med antagandet att tabellen är korrekt. Sedan behöver vi endast bevisa att tabellen är korrekt, 20
det gör beviset enklare. Vi kommer dock att indirekt ha en tabell med regler i vår algoritm. Indata till vår typhärledare är det Lisp-uttryck som vi ska härleda, uttrycket är ett S-uttryck och vi kommer att använda de interna funktionerna i Lisp för att känna igen kod. För att granska om ett uttryck är en variabel ska det vara en symbol skilt från nil och t. Liknande denitioner kan göras för andra uttryck, exempelvis ska defun vara en lista där det första elementet ska vara lika med symbolen defun. Detta räcker för att känna igen syntaktiskt korrekt kod men vi kan välja att göra nogrannare tester och ge felmeddelanden om programmet är syntaktiskt inkorrekt. Alla uttryck känns igen på liknande sätt. Kom ihåg att ' x och #'x är syntaxtiskt socker för (quote x) respektive (function x) och känns igen genom att titta efter symbolerna quote och function. ( defun l i s p v a r i a b l e? ( expr ) ( and ( symbolp expr ) ( not ( e q u a l t expr ) ) ( not ( l i s t p expr ) ) ) ) ( defun l i s p defun? ( expr ) ( and ( l i s t p expr ) ( eq ( c a r expr ) ' defun ) ) ) ( defun quoted? ( expr ) ( and ( l i s t p expr ) ( e q u a l ( f i r s t expr ) ' quote ) ) ) Nu när vi kan känna igen vad det är för uttryck kan vi granska vilka regler som gäller och anropa den funktion tar hand om uttrycket. Precis som i teoridelen vill vi ha ett uttryck, typuttryck och en typomgivning. När vi startar typhärledningen kommer typuttrycket att vara ett object-typuttryck. Det går att se variabeln som att vi skickar in det typuttryck som vi förväntar oss. Vi har inget returvärde från algoritmen då vi gör allt destruktivt i typomgivningen (kom ihåg att vi har en omgivning för ekvationer). ( defun generate c o n s t r a i n t s! ( expr type env ) ( cond ( ( l i s p c o n s t a n t? expr ) ( constant c o n s t r a i n t s! expr type env ) ) ( ( l i s p v a r i a b l e? expr ) ( v a r i a b l e c o n s t r a i n t s! expr type env ) ) ( ( l i s p lambda? expr ) ( lambda c o n s t r a i n t s! expr type env ) ) ( ( l i s p f u n c t i o n? expr ) ( f u n c t i o n c o n s t r a i n t s! expr type env ) ) ( ( l i s p f u n c a l l? expr ) ( f u n c a l l c o n s t r a i n t s! expr type env ) ) ( ( l i s p progn? expr ) ( progn c o n s t r a i n t s! expr type env ) ) ( ( l i s p i f? expr ) ( i f c o n s t r a i n t s! expr type env ) ) ( ( l i s p cond? expr ) ( cond c o n s t r a i n t s! expr type env ) ) ( ( l i s p l e t? expr ) ( l e t c o n s t r a i n t s! expr type env ) ) ( ( l i s p defun? expr ) ( defun c o n s t r a i n t s! expr type env ) ) ( ( l i s p l a b e l s? expr ) ( l a b e l s c o n s t r a i n t s! expr type env ) ) 21
( ( l i s p c a l l? expr ) ( c a l l c o n s t r a i n t s! expr type env ) ) ( t ( e r r o r ' unknown e x p r e s s i o n ) ) ) ) Det är viktigt att anropa lisp call? sist då den även fångar upp special forms. Detta är vår motsvarighet till tabellen i teoridelen, reglerna är funktionerna som anropas om testet lyckas. Vi anropar den funktion som hanterar reglerna för uttrycket. Om denna regel behöver typhärleda något deluttryck anropas generate constraints! igen rekursivt. Vi kommer då på ett enkelt sätt stega igenom programmet likt en interpretator. Ingen kö likt Wands algoritm i teoridelen behövs. Om uttrycket är en variabel kommer vi att slå upp typuttrycket i variabelomgivningen och skapa en ekvation med det typuttryck vi ck. För exempelvis en if -sats plockar vi istället ut villkoret och grenarnas uttryck. För det villkorliga uttrycket förväntar vi oss ett booleskt typuttryck så vi skapar ett nytt typuttryck och skickar in det. För grenarna förväntas samma typuttryck som if -uttrycket så vi använder det typuttryck vi ck. Vi anropar sedan generate constraints! med de uttrycken. Vi gör ingen test om den falska grenen nns eller inte, för om den inte nns returneras nil som vi inte vet hur vi ska tolka. ( defun v a r i a b l e c o n s t r a i n t s! ( expr type env ) ( add c o n s t r a i n t! ( make c o n s t r a i n t type ( get v a r i a b l e v a l u e expr env ) ) env ) ' ok ) ( defun i f c o n s t r a i n t s! ( expr type env ) ( l e t ( ( c o n d i t i o n a l ( second expr ) ) ( then ( t h i r d expr ) ) ( e l s e ( f o u r t h expr ) ) ) ( generate c o n s t r a i n t s! c o n d i t i o n a l ( boolean type ) env ) ( generate c o n s t r a i n t s! then type env ) ( generate c o n s t r a i n t s! e l s e type env ) ' ok ) ) Funktionen add constraint! lägger till ekvationen i ekvationsomgivningen. boolean type skapar ett booleskt-typuttryck. För att om det är en konstant behöver vi endast använda de inbyggda Lisp-typkontrollerna. Vi kan dock inte granska om någonting är en lista med dessa, då de esta uttryck är listor och vi behöver granska om uttrycket är en quote'ad lista. Till en början kommer vi inte att stödja listor då vi endast vill ha homogena listor och att få ut typen av en lista kräver extra arbete. Kom ihåg att en quote'ad konstant är samma sak som en icke-quote'ad konstant (förutom listor). 22
( defun l i s p c o n s t a n t? ( expr ) ( or ( quoted? expr ) ( numberp expr ) ( c h a r a c t e r p expr ) ( s t r i n g p expr ) ( booleanp expr ) ) ) ( defun get constant type ( expr ) ( l a b e l s ( ( aux ( expr ) ( cond ( ( booleanp expr ) ( boolean type ) ) ( ( symbolp expr ) ( symbol type ) ) ( ( s t r i n g p expr ) ( s t r i n g type ) ) ( ( c h a r a c t e r p expr ) ( c h a r a c t e r type ) ) ( ( i n t e g e r p expr ) ( i n t e g e r type ) ) ( ( r a t i o n a l p expr ) ( r a t i o n a l type ) ) ( ( r e a l p expr ) ( r e a l type ) ) ( ( complexp expr ) ( complex type ) ) ( ( numberp expr ) ( number type ) ) ( t ( e r r o r ' not a c o n s t a n t ) ) ) ) ) ( i f ( quoted? expr ) ( aux ( unquote expr ) ) ( aux expr ) ) ) ) Funktionen unquote hämtar ut det quote'ade uttrycket (andra elementet). Nu när vi kan granska om någonting är en konstant och få ut typuttryck för konstanten kan vi enkelt skapa en regel som skapar en ekvation. ( defun constant c o n s t r a i n t s! ( expr type env ) ( add c o n s t r a i n t! ( make c o n s t r a i n t type ( get constant type expr ) ) env ) ' ok ) Hittills har vi endast gått igenom konstruktioner som inte utökar typomgivningen. Vi ska därför undersöka defun som utökar typomgivningen och lägger till en ny bindning i funktionsomgivningen. Uttrycket defun har ett namn, en formell parameterlista och en kropp. Vi plockar ut respektive del. Kom ihåg att uttrycket i sig returnerar en symbol med funktionens namn och inte vad funktionen returnerar. Vi skapar ett object-typuttryck för varje variabel i parameterlistan, skapar ett object-typuttryck för kroppen samt en för funktionen där funktionen går från de typuttryck vi skapade för parameterlistan till typuttrycket vi skapade för kroppen. Vi måste lägga till en ny bindning i den globala funktionsomgivningen med namnet och funktionstyputtrycket. Sedan utökas typomgivningen där variabelnamnen binds till deras typuttryck. Vi har också en implicit progn i kroppen, det räcker med att lägga till symbolen progn i kroppen och vid anropet till generate constraints! kommer rätt regel ta hand om kroppen. Vi tar inte upp progn i rapporten men denitionen har inga överraskningar. 23